Jason Rivard 4 лет назад
Родитель
Сommit
542b59dd73
100 измененных файлов с 2143 добавлено и 1595 удалено
  1. 0 1
      build/checkstyle-import.xml
  2. 1 1
      client/angular/package.json
  3. 1 1
      data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java
  4. 1 1
      data-service/src/main/java/password/pwm/receiver/Settings.java
  5. 1 1
      data-service/src/main/java/password/pwm/receiver/Storage.java
  6. 1 1
      data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java
  7. 4 2
      pom.xml
  8. 1 5
      server/pom.xml
  9. 2 1
      server/src/main/java/password/pwm/AppProperty.java
  10. 5 12
      server/src/main/java/password/pwm/PwmAboutProperty.java
  11. 106 71
      server/src/main/java/password/pwm/PwmApplication.java
  12. 1 23
      server/src/main/java/password/pwm/PwmConstants.java
  13. 31 20
      server/src/main/java/password/pwm/PwmDomain.java
  14. 18 15
      server/src/main/java/password/pwm/PwmEnvironment.java
  15. 0 3
      server/src/main/java/password/pwm/PwmEnvironmentUtils.java
  16. 11 7
      server/src/main/java/password/pwm/bean/DomainID.java
  17. 2 0
      server/src/main/java/password/pwm/bean/PrivateKeyCertificate.java
  18. 2 2
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  19. 6 4
      server/src/main/java/password/pwm/bean/UserIdentity.java
  20. 42 17
      server/src/main/java/password/pwm/config/AppConfig.java
  21. 17 29
      server/src/main/java/password/pwm/config/DomainConfig.java
  22. 9 5
      server/src/main/java/password/pwm/config/PwmSetting.java
  23. 3 2
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  24. 2 0
      server/src/main/java/password/pwm/config/PwmSettingFlag.java
  25. 36 2
      server/src/main/java/password/pwm/config/PwmSettingMetaDataReader.java
  26. 1 1
      server/src/main/java/password/pwm/config/PwmSettingSyntax.java
  27. 20 26
      server/src/main/java/password/pwm/config/PwmSettingTemplateSet.java
  28. 14 263
      server/src/main/java/password/pwm/config/SettingReader.java
  29. 345 0
      server/src/main/java/password/pwm/config/StoredSettingReader.java
  30. 2 3
      server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  31. 1 1
      server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java
  32. 2 2
      server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java
  33. 3 4
      server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java
  34. 11 2
      server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  35. 5 5
      server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java
  36. 4 4
      server/src/main/java/password/pwm/config/profile/AbstractProfile.java
  37. 3 3
      server/src/main/java/password/pwm/config/profile/ChallengeProfile.java
  38. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  39. 5 6
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  40. 41 25
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  41. 2 2
      server/src/main/java/password/pwm/config/stored/ConfigSearchMachine.java
  42. 14 15
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  43. 48 36
      server/src/main/java/password/pwm/config/stored/ConfigurationReader.java
  44. 6 2
      server/src/main/java/password/pwm/config/stored/StoredConfigData.java
  45. 12 31
      server/src/main/java/password/pwm/config/stored/StoredConfigKey.java
  46. 66 29
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  47. 6 4
      server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java
  48. 16 8
      server/src/main/java/password/pwm/config/stored/StoredConfigZipXmlSerializer.java
  49. 5 4
      server/src/main/java/password/pwm/config/stored/StoredConfiguration.java
  50. 19 5
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  51. 59 33
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  52. 17 22
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  53. 64 51
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  54. 7 6
      server/src/main/java/password/pwm/config/value/ActionValue.java
  55. 58 13
      server/src/main/java/password/pwm/config/value/FileValue.java
  56. 4 4
      server/src/main/java/password/pwm/config/value/FormValue.java
  57. 2 2
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  58. 2 2
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  59. 12 3
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  60. 1 1
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  61. 1 1
      server/src/main/java/password/pwm/config/value/StoredValueEncoder.java
  62. 7 1
      server/src/main/java/password/pwm/config/value/StringArrayValue.java
  63. 28 12
      server/src/main/java/password/pwm/config/value/StringValue.java
  64. 9 1
      server/src/main/java/password/pwm/config/value/ValueFactory.java
  65. 3 2
      server/src/main/java/password/pwm/config/value/ValueTypeConverter.java
  66. 3 3
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  67. 2 1
      server/src/main/java/password/pwm/config/value/X509CertificateValue.java
  68. 1 1
      server/src/main/java/password/pwm/config/value/data/FormConfiguration.java
  69. 4 11
      server/src/main/java/password/pwm/error/ErrorInformation.java
  70. 2 2
      server/src/main/java/password/pwm/error/PwmError.java
  71. 8 0
      server/src/main/java/password/pwm/error/PwmUnrecoverableException.java
  72. 21 14
      server/src/main/java/password/pwm/health/ApplianceStatusChecker.java
  73. 3 3
      server/src/main/java/password/pwm/health/CertificateChecker.java
  74. 96 59
      server/src/main/java/password/pwm/health/ConfigurationChecker.java
  75. 16 10
      server/src/main/java/password/pwm/health/DatabaseStatusChecker.java
  76. 2 2
      server/src/main/java/password/pwm/health/HealthMessage.java
  77. 7 2
      server/src/main/java/password/pwm/health/HealthMonitorSettings.java
  78. 5 13
      server/src/main/java/password/pwm/health/HealthRecord.java
  79. 43 55
      server/src/main/java/password/pwm/health/HealthService.java
  80. 4 4
      server/src/main/java/password/pwm/health/HealthStatus.java
  81. 2 2
      server/src/main/java/password/pwm/health/HealthTopic.java
  82. 40 0
      server/src/main/java/password/pwm/health/HealthUtils.java
  83. 13 5
      server/src/main/java/password/pwm/health/JavaChecker.java
  84. 23 21
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  85. 18 12
      server/src/main/java/password/pwm/health/LocalDBHealthChecker.java
  86. 30 34
      server/src/main/java/password/pwm/http/ContextManager.java
  87. 34 30
      server/src/main/java/password/pwm/http/HttpEventManager.java
  88. 9 9
      server/src/main/java/password/pwm/http/IdleTimeoutCalculator.java
  89. 55 0
      server/src/main/java/password/pwm/http/PwmCookiePath.java
  90. 51 30
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  91. 2 118
      server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  92. 133 65
      server/src/main/java/password/pwm/http/PwmRequest.java
  93. 1 1
      server/src/main/java/password/pwm/http/PwmRequestContext.java
  94. 123 13
      server/src/main/java/password/pwm/http/PwmResponse.java
  95. 14 15
      server/src/main/java/password/pwm/http/PwmSession.java
  96. 25 22
      server/src/main/java/password/pwm/http/PwmSessionFactory.java
  97. 101 97
      server/src/main/java/password/pwm/http/PwmURL.java
  98. 17 66
      server/src/main/java/password/pwm/http/SessionManager.java
  99. 4 3
      server/src/main/java/password/pwm/http/auth/HttpAuthenticationUtilities.java
  100. 2 10
      server/src/main/java/password/pwm/http/filter/AbstractPwmFilter.java

+ 0 - 1
build/checkstyle-import.xml

@@ -74,7 +74,6 @@
     <allow pkg="org.apache.http"/>
     <allow pkg="org.apache.commons"/>
     <allow pkg="org.jasig.cas"/>
-    <allow pkg="net.iharder"/>
     <allow pkg="org.jetbrains.annotations"/>
     <allow pkg="com.novell.ldap"/>
     <allow pkg="java.security"/>

+ 1 - 1
client/angular/package.json

@@ -8,7 +8,7 @@
         "npm": ">=3.9"
     },
     "scripts": {
-        "build": "webpack --mode=production",
+        "build": "webpack --mode=development",
         "clean": "rimraf dist/",
         "test": "karma start test/karma.conf.js",
         "test-single-run": "karma start test/karma.conf.js --singleRun --no-auto-watch",

+ 1 - 1
data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java

@@ -173,7 +173,7 @@ class FtpDataIngestor
 
         ftpClient.connect( settings.getSetting( Settings.Setting.ftpSite ) );
         LOGGER.info( "ftp connect complete" );
-        if ( !StringUtil.isEmpty( settings.getSetting( Settings.Setting.ftpUser ) ) && !StringUtil.isEmpty( settings.getSetting( Settings.Setting.ftpPassword ) ) )
+        if ( StringUtil.notEmpty( settings.getSetting( Settings.Setting.ftpUser ) ) && StringUtil.notEmpty( settings.getSetting( Settings.Setting.ftpPassword ) ) )
         {
             final boolean loggedInSuccess = ftpClient.login( settings.getSetting( Settings.Setting.ftpUser ), settings.getSetting( Settings.Setting.ftpPassword ) );
             LOGGER.info( "ftp login complete, success=" + loggedInSuccess );

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

@@ -95,6 +95,6 @@ public class Settings
     public boolean isFtpEnabled( )
     {
         final String value = settings.get( Setting.ftpSite );
-        return !StringUtil.isEmpty( value );
+        return StringUtil.notEmpty( value );
     }
 }

+ 1 - 1
data-service/src/main/java/password/pwm/receiver/Storage.java

@@ -115,7 +115,7 @@ public class Storage
             if ( v != null )
             {
                 final String string = StringBinding.entryToString( new ArrayByteIterable( v ) );
-                if ( !StringUtil.isEmpty( string ) )
+                if ( StringUtil.notEmpty( string ) )
                 {
                     return JsonUtil.deserialize( string, TelemetryPublishBean.class );
                 }

+ 1 - 1
data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java

@@ -52,7 +52,7 @@ public class TelemetryViewerServlet extends HttpServlet
 
         {
             final String errorState = app.getStatus().getErrorState();
-            if ( !StringUtil.isEmpty( errorState ) )
+            if ( StringUtil.notEmpty( errorState ) )
             {
                 resp.sendError( 500, errorState );
                 final String htmlBody = "<html>Error: " + errorState + "</html>";

+ 4 - 2
pom.xml

@@ -34,6 +34,7 @@
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <maven.compiler.source>11</maven.compiler.source>
         <maven.compiler.target>11</maven.compiler.target>
+        <maven.compiler.release>11</maven.compiler.release>
         <maven.javadoc.skip>true</maven.javadoc.skip>
 
         <!-- profile managed values -->
@@ -224,6 +225,7 @@
                 <configuration>
                     <source>${maven.compiler.source}</source>
                     <target>${maven.compiler.target}</target>
+                    <release>${maven.compiler.release}</release>
                     <showWarnings>true</showWarnings>
                 </configuration>
             </plugin>
@@ -332,7 +334,7 @@
                 </configuration>
                 <executions>
                     <execution>
-                        <phase>verify</phase>
+                        <phase>test</phase>
                         <goals>
                             <goal>check</goal>
                         </goals>
@@ -363,7 +365,7 @@
             <plugin>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>6.0.4</version>
+                <version>6.0.5</version>
                 <executions>
                     <execution>
                         <goals>

+ 1 - 5
server/pom.xml

@@ -45,6 +45,7 @@
                 <configuration>
                     <source>${maven.compiler.source}</source>
                     <target>${maven.compiler.target}</target>
+                    <release>${maven.compiler.release}</release>
                     <showWarnings>true</showWarnings>
                     <compilerArgs>
                         <arg>-Xmaxwarns</arg>
@@ -308,11 +309,6 @@
             <artifactId>jcl-core</artifactId>
             <version>2.8</version>
         </dependency>
-        <dependency>
-            <groupId>net.iharder</groupId>
-            <artifactId>base64</artifactId>
-            <version>2.3.9</version>
-        </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>

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

@@ -65,7 +65,7 @@ public enum AppProperty
     CLIENT_PW_SHOW_REVERT_TIMEOUT                   ( "client.pwShowRevertTimeout" ),
     CLIENT_JS_ENABLE_HTML5DIALOG                    ( "client.js.enableHtml5Dialog" ),
     CLIENT_JSP_SHOW_ICONS                           ( "client.jsp.showIcons" ),
-    CONFIG_MAX_JDBC_JAR_SIZE                        ( "config.maxJdbcJarSize" ),
+    CONFIG_MAX_FILEVALUE_SIZE                       ( "config.max.fileValue.size" ),
     CONFIG_RELOAD_ON_CHANGE                         ( "config.reloadOnChange" ),
     CONFIG_MAX_PERSISTENT_LOGIN_SECONDS             ( "config.maxPersistentLoginSeconds" ),
     CONFIG_HISTORY_MAX_ITEMS                        ( "config.login.history.maxEvents" ),
@@ -237,6 +237,7 @@ public enum AppProperty
     LDAP_SEARCH_PARALLEL_FACTOR                     ( "ldap.search.parallel.factor" ),
     LDAP_SEARCH_PARALLEL_THREAD_MAX                 ( "ldap.search.parallel.threadMax" ),
     LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME        ( "ldap.oracle.postTempPasswordUseCurrentTime" ),
+    LOGGING_OUTPUT_CONFIGURATION                    ( "logging.outputConfiguration" ),
     LOGGING_PATTERN                                 ( "logging.pattern" ),
     LOGGING_EXTRA_PERIODIC_THREAD_DUMP_INTERVAL     ( "logging.extra.periodicThreadDumpIntervalSeconds" ),
     LOGGING_FILE_MAX_SIZE                           ( "logging.file.maxSize" ),

+ 5 - 12
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -22,7 +22,7 @@ package password.pwm;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
-import password.pwm.svc.sessiontrack.SessionTrackService;
+import password.pwm.ldap.LdapConnectionService;
 import password.pwm.util.db.DatabaseService;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.FileSystemUtility;
@@ -35,7 +35,6 @@ import java.nio.charset.Charset;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.function.Function;
@@ -61,9 +60,9 @@ public enum PwmAboutProperty
     app_sharedHistorySize( null, pwmApplication -> Long.toString( pwmApplication.getSharedHistoryManager().size() ) ),
     app_sharedHistoryOldestTime( null, pwmApplication -> format( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) ),
     app_emailQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getEmailQueue().queueSize() ) ),
-    app_emailQueueOldestTime( null, pwmApplication -> format( Date.from( pwmApplication.getEmailQueue().eldestItem() ) ) ),
+    app_emailQueueOldestTime( null, pwmApplication -> format( pwmApplication.getEmailQueue().eldestItem() ) ),
     app_smsQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getSmsQueue().queueSize() ) ),
-    app_smsQueueOldestTime( null, pwmApplication -> format( Date.from( pwmApplication.getSmsQueue().eldestItem() ) ) ),
+    app_smsQueueOldestTime( null, pwmApplication -> format( pwmApplication.getSmsQueue().eldestItem() ) ),
     app_syslogQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getAuditManager().syslogQueueSize() ) ),
     app_localDbLogSize( null, pwmApplication -> Integer.toString( pwmApplication.getLocalDBLogger().getStoredEventCount() ) ),
     app_localDbLogOldestTime( null, pwmApplication -> format( pwmApplication.getLocalDBLogger().getTailDate() ) ),
@@ -72,8 +71,8 @@ public enum PwmAboutProperty
     app_configurationRestartCounter( null, pwmApplication -> Integer.toString( pwmApplication.getPwmEnvironment().getContextManager().getRestartCount() ) ),
     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 -> Long.toString( SessionTrackService.totalLdapConnectionCount( pwmApplication ) ) ),
+    app_ldapProfileCount( null, pwmApplication -> Integer.toString( LdapConnectionService.totalLdapProfileCount( pwmApplication ) ) ),
+    app_ldapConnectionCount( null, pwmApplication -> Long.toString( LdapConnectionService.totalLdapConnectionCount( pwmApplication ) ) ),
     app_activeSessionCount( "Active Session Count", pwmApplication -> Integer.toString( pwmApplication.getSessionTrackService().sessionCount() ) ),
     app_activeRequestCount( "Active Request Count", pwmApplication -> Integer.toString( pwmApplication.getActiveServletRequests().get() ) ),
 
@@ -159,12 +158,6 @@ public enum PwmAboutProperty
         return Collections.unmodifiableMap( returnMap );
     }
 
-    private static String format( final Date date )
-    {
-        return format( date == null ? null : date.toInstant() );
-    }
-
-
     private static String format( final Instant date )
     {
         if ( date != null )

+ 106 - 71
server/src/main/java/password/pwm/PwmApplication.java

@@ -26,16 +26,16 @@ import password.pwm.bean.SmsItemBean;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingMetaDataReader;
 import password.pwm.config.PwmSettingScope;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 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.resource.ResourceServletService;
+import password.pwm.health.HealthService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceEnum;
@@ -47,8 +47,8 @@ import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditService;
 import password.pwm.svc.event.SystemAuditRecord;
 import password.pwm.svc.httpclient.HttpClientService;
-import password.pwm.svc.intruder.IntruderManager;
-import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.intruder.IntruderService;
+import password.pwm.svc.intruder.IntruderRecordType;
 import password.pwm.svc.node.NodeService;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
@@ -60,15 +60,15 @@ import password.pwm.svc.stats.Statistic;
 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.SharedHistoryService;
 import password.pwm.svc.wordlist.WordlistService;
-import password.pwm.util.DailySummaryJob;
 import password.pwm.util.MBeanUtility;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseService;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -102,10 +102,11 @@ 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.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 public class PwmApplication
 {
@@ -150,12 +151,12 @@ public class PwmApplication
         }
     }
 
-    public static Optional<String> deriveLocalServerHostname( final DomainConfig domainConfig )
+    public static Optional<String> deriveLocalServerHostname( final AppConfig appConfig )
     {
-        if ( domainConfig != null )
+        if ( appConfig != null )
         {
-            final String siteUrl = domainConfig.readSettingAsString( PwmSetting.PWM_SITE_URL );
-            if ( !StringUtil.isEmpty( siteUrl ) )
+            final String siteUrl = appConfig.readSettingAsString( PwmSetting.PWM_SITE_URL );
+            if ( StringUtil.notEmpty( siteUrl ) )
             {
                 try
                 {
@@ -252,15 +253,7 @@ 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 ) );
-        }
+        initAllDomains();
 
         final boolean skipPostInit = pwmEnvironment.isInternalRuntimeInstance()
                 || pwmEnvironment.getFlags().contains( PwmEnvironment.ApplicationFlag.CommandLineInstance );
@@ -269,14 +262,31 @@ public class PwmApplication
         {
             final TimeDuration totalTime = TimeDuration.fromCurrent( startTime );
             LOGGER.info( () -> PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION + " open for bidness! (" + totalTime.asCompactString() + ")" );
-            StatisticsManager.incrementStat( this.getDefaultDomain(), Statistic.PWM_STARTUPS );
+            StatisticsManager.incrementStat( this, Statistic.PWM_STARTUPS );
             LOGGER.debug( () -> "buildTime=" + PwmConstants.BUILD_TIME + ", javaLocale=" + Locale.getDefault() + ", DefaultLocale=" + PwmConstants.DEFAULT_LOCALE );
 
-            pwmScheduler.immediateExecuteInNewThread( this::postInitTasks, this.getClass().getSimpleName() + " postInit tasks" );
+            pwmScheduler.immediateExecuteRunnableInNewThread( this::postInitTasks, this.getClass().getSimpleName() + " postInit tasks" );
         }
 
     }
 
+    private void initAllDomains()
+            throws PwmUnrecoverableException
+    {
+        final Instant domainInitStartTime = Instant.now();
+        LOGGER.trace( () -> "beginning domain initializations" );
+
+        final List<Callable<?>> callables = domains.values().stream().<Callable<?>>map( pwmDomain -> () ->
+        {
+            pwmDomain.initialize();
+            return null;
+        } ).collect( Collectors.toList() );
+        pwmScheduler.executeImmediateThreadPerJobAndAwaitCompletion( callables, "domain initializer" );
+
+        LOGGER.trace( () -> "completed domain initialization for all domains", () -> TimeDuration.fromCurrent( domainInitStartTime ) );
+    }
+
+
     public void reInit( final PwmEnvironment pwmEnvironment )
             throws PwmException
     {
@@ -292,17 +302,20 @@ public class PwmApplication
     {
         final Instant startTime = Instant.now();
 
-        getPwmScheduler().immediateExecuteInNewThread( UserAgentUtils::initializeCache, "initialize useragent cache" );
-        getPwmScheduler().immediateExecuteInNewThread( SettingDataMaker::initializeCache, "initialize PwmSetting metadata" );
+        getPwmScheduler().immediateExecuteRunnableInNewThread( UserAgentUtils::initializeCache, "initialize useragent cache" );
+        getPwmScheduler().immediateExecuteRunnableInNewThread( PwmSettingMetaDataReader::initCache, "initialize PwmSetting cache" );
 
-        outputConfigurationToLog( this );
 
-        outputNonDefaultPropertiesToLog( this );
+        if ( Boolean.parseBoolean( getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) ) )
+        {
+            outputConfigurationToLog( this );
+            outputNonDefaultPropertiesToLog( this );
+        }
 
         // send system audit event
         try
         {
-            final SystemAuditRecord auditRecord = new AuditRecordFactory( this.getDefaultDomain() ).createSystemAuditRecord(
+            final SystemAuditRecord auditRecord = new AuditRecordFactory( this ).createSystemAuditRecord(
                     AuditEvent.STARTUP,
                     null
             );
@@ -325,7 +338,7 @@ public class PwmApplication
 
         try
         {
-            this.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
+            this.getIntruderService().clear( IntruderRecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
         }
         catch ( final Exception e )
         {
@@ -354,12 +367,6 @@ public class PwmApplication
         }
 
         MBeanUtility.registerMBean( this );
-
-        {
-            final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( this, PwmDomain.class );
-            pwmScheduler.scheduleDailyZuluZeroStartJob( new DailySummaryJob( this.getDefaultDomain() ), executorService, TimeDuration.ZERO );
-        }
-
         LOGGER.trace( () -> "completed post init tasks", () -> TimeDuration.fromCurrent( startTime ) );
     }
 
@@ -368,7 +375,7 @@ public class PwmApplication
         return new PwmApplication( pwmEnvironment );
     }
 
-    public Map<DomainID, PwmDomain> getDomains()
+    public Map<DomainID, PwmDomain> domains()
     {
         return domains;
     }
@@ -398,9 +405,19 @@ public class PwmApplication
         return runtimeNonce;
     }
 
-    public PwmDomain getDefaultDomain()
+    public PwmDomain getAdminDomain()
+            throws PwmUnrecoverableException
     {
-        return getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER );
+        final Optional<DomainConfig> adminDomainConfig = getConfig().getDomainConfigs().values().stream()
+                .filter( DomainConfig::isAdministrativeDomain )
+                .findFirst();
+
+        if ( adminDomainConfig.isPresent() )
+        {
+            return domains().get( adminDomainConfig.get().getDomainID() );
+        }
+
+        throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "administrative domain is not defined" );
     }
 
     public void shutdown( )
@@ -410,14 +427,12 @@ public class PwmApplication
 
     public void shutdown( final boolean keepServicesRunning )
     {
-        pwmScheduler.shutdown();
-
         LOGGER.warn( () -> "shutting down" );
         {
             // send system audit event
             try
             {
-                final SystemAuditRecord auditRecord = new AuditRecordFactory( this.getDefaultDomain() ).createSystemAuditRecord(
+                final SystemAuditRecord auditRecord = new AuditRecordFactory( this ).createSystemAuditRecord(
                         AuditEvent.SHUTDOWN,
                         null
                 );
@@ -436,12 +451,21 @@ public class PwmApplication
 
         if ( !keepServicesRunning )
         {
-            pwmServiceManager.shutdownAllServices();
-
-            for ( final PwmDomain pwmDomain : domains.values() )
+            try
+            {
+                final List<Callable<?>> callables = domains.values().stream().<Callable<?>>map( pwmDomain -> () ->
+                {
+                    pwmDomain.shutdown();
+                    return null;
+                } ).collect( Collectors.toList() );
+                pwmScheduler.executeImmediateThreadPerJobAndAwaitCompletion( callables, "domain shutdown task" );
+            }
+            catch ( final PwmUnrecoverableException e )
             {
-                pwmDomain.shutdown();
+                LOGGER.error( () -> "error shutting down domain services: " + e.getMessage(), e );
             }
+
+            pwmServiceManager.shutdownAllServices();
         }
 
         if ( localDBLogger != null )
@@ -480,6 +504,8 @@ public class PwmApplication
             fileLocker.releaseFileLock();
         }
 
+        pwmScheduler.shutdown();
+
         LOGGER.info( () -> PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION + " closed for bidness, cya!" );
     }
 
@@ -570,7 +596,7 @@ public class PwmApplication
         }
     }
 
-    private static void outputConfigurationToLog( final PwmApplication pwmDomain )
+    private static void outputConfigurationToLog( final PwmApplication pwmApplication )
     {
         final Instant startTime = Instant.now();
 
@@ -580,10 +606,11 @@ public class PwmApplication
             return " " + entry.getKey() + "\n   " + spacedValue + "\n";
         };
 
-        final StoredConfiguration storedConfiguration = pwmDomain.getConfig().getStoredConfiguration();
+        final StoredConfiguration storedConfiguration = pwmApplication.getConfig().getStoredConfiguration();
+        final List<StoredConfigKey> keys = CollectionUtil.iteratorToStream( storedConfiguration.keys() ).collect( Collectors.toList() );
         final Map<String, String> debugStrings = StoredConfigurationUtil.makeDebugMap(
                 storedConfiguration,
-                storedConfiguration.keys(),
+                keys,
                 PwmConstants.DEFAULT_LOCALE );
 
         LOGGER.trace( () -> "--begin current configuration output--" );
@@ -599,7 +626,7 @@ public class PwmApplication
         final Instant startTime = Instant.now();
 
         final Map<AppProperty, String> nonDefaultProperties = pwmApplication.getConfig().readAllNonDefaultAppProperties();
-        if ( !JavaHelper.isEmpty( nonDefaultProperties ) )
+        if ( !CollectionUtil.isEmpty( nonDefaultProperties ) )
         {
             LOGGER.trace( () -> "--begin non-default app properties output--" );
             nonDefaultProperties.entrySet().stream()
@@ -637,8 +664,11 @@ public class PwmApplication
 
         try
         {
-            final String strValue = localDB.get( LocalDB.DB.PWM_META, appAttribute.getKey() );
-            return Optional.of( JsonUtil.deserialize( strValue, returnClass ) );
+            final Optional<String> strValue = localDB.get( LocalDB.DB.PWM_META, appAttribute.getKey() );
+            if ( strValue.isPresent() )
+            {
+                return Optional.of( JsonUtil.deserialize( strValue.get(), returnClass ) );
+            }
         }
         catch ( final Exception e )
         {
@@ -698,13 +728,13 @@ public class PwmApplication
             try
             {
                 final Optional<String> storedDateStr = readAppAttribute( AppAttribute.INSTALL_DATE, String.class );
-                if ( !storedDateStr.isPresent() )
+                if ( storedDateStr.isPresent() )
                 {
-                    writeAppAttribute( AppAttribute.INSTALL_DATE, String.valueOf( startupTime.toEpochMilli() ) );
+                    return Instant.ofEpochMilli( Long.parseLong( storedDateStr.get() ) );
                 }
                 else
                 {
-                    return Instant.ofEpochMilli( Long.parseLong( storedDateStr.get() ) );
+                    writeAppAttribute( AppAttribute.INSTALL_DATE, String.valueOf( startupTime.toEpochMilli() ) );
                 }
             }
             catch ( final Exception e )
@@ -751,14 +781,14 @@ public class PwmApplication
         return newInstanceID;
     }
 
-    public SharedHistoryManager getSharedHistoryManager( )
+    public SharedHistoryService getSharedHistoryManager( )
     {
-        return ( SharedHistoryManager ) pwmServiceManager.getService( PwmServiceEnum.SharedHistoryManager );
+        return ( SharedHistoryService ) pwmServiceManager.getService( PwmServiceEnum.SharedHistoryManager );
     }
 
-    public IntruderManager getIntruderManager( )
+    public IntruderService getIntruderService( )
     {
-        return ( IntruderManager ) pwmServiceManager.getService( PwmServiceEnum.IntruderManager );
+        return ( IntruderService ) pwmServiceManager.getService( PwmServiceEnum.IntruderManager );
     }
 
     public LocalDBLogger getLocalDBLogger( )
@@ -766,9 +796,9 @@ public class PwmApplication
         return localDBLogger;
     }
 
-    public HealthMonitor getHealthMonitor( )
+    public HealthService getHealthMonitor( )
     {
-        return ( HealthMonitor ) pwmServiceManager.getService( PwmServiceEnum.HealthMonitor );
+        return ( HealthService ) pwmServiceManager.getService( PwmServiceEnum.HealthMonitor );
     }
 
     public HttpClientService getHttpClientService()
@@ -781,10 +811,17 @@ public class PwmApplication
         final List<PwmService> pwmServices = new ArrayList<>();
         pwmServices.add( this.localDBLogger );
         pwmServices.addAll( this.pwmServiceManager.getRunningServices() );
-        pwmServices.remove( null );
         return Collections.unmodifiableList( pwmServices );
     }
 
+    public List<PwmService> getAppAndDomainPwmServices( )
+    {
+        final List<PwmService> pwmServices = new ArrayList<>( getPwmServices() );
+        domains().values().forEach( domain -> pwmServices.addAll( domain.getPwmServices() ) );
+        return Collections.unmodifiableList( pwmServices );
+
+    }
+
     public WordlistService getWordlistService( )
     {
         return ( WordlistService ) pwmServiceManager.getService( PwmServiceEnum.WordlistManager );
@@ -845,11 +882,6 @@ public class PwmApplication
         return ( SessionTrackService ) pwmServiceManager.getService( PwmServiceEnum.SessionTrackService );
     }
 
-    public ResourceServletService getResourceServletService( )
-    {
-        return ( ResourceServletService ) pwmServiceManager.getService( PwmServiceEnum.ResourceServletService );
-    }
-
     public DatabaseAccessor getDatabaseAccessor( )
 
             throws PwmUnrecoverableException
@@ -883,7 +915,6 @@ public class PwmApplication
         return ( SystemSecureService ) pwmServiceManager.getService( PwmServiceEnum.SystemSecureService );
     }
 
-
     public Instant getStartupTime( )
     {
         return startupTime;
@@ -899,6 +930,11 @@ public class PwmApplication
         return localDB;
     }
 
+    public boolean isMultiDomain()
+    {
+        return this.getConfig().isMultiDomain();
+    }
+
     public void sendSmsUsingQueue(
             final String to,
             final String message,
@@ -964,8 +1000,7 @@ public class PwmApplication
             }
         }
 
-        public static Map<DomainID, PwmDomain> initializeDomains( final PwmApplication pwmApplication )
-                throws PwmUnrecoverableException
+        private static Map<DomainID, PwmDomain> initializeDomains( final PwmApplication pwmApplication )
         {
             final Map<DomainID, PwmDomain> domainMap = new TreeMap<>();
             for ( final String domainIdString : pwmApplication.getPwmEnvironment().getConfig().getDomainIDs() )
@@ -1004,8 +1039,8 @@ public class PwmApplication
                 }
 
                 PwmLogManager.initializeLogger(
-                        pwmApplication.getDefaultDomain(),
-                        pwmApplication.getDefaultDomain().getConfig(),
+                        pwmApplication,
+                        pwmApplication.getConfig(),
                         log4jFile,
                         consoleLevel,
                         pwmEnvironment.getApplicationPath(),

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

@@ -22,14 +22,12 @@ package password.pwm;
 
 import com.novell.ldapchai.ChaiConstant;
 import org.apache.commons.csv.CSVFormat;
-import password.pwm.bean.DomainID;
 import password.pwm.util.java.StringUtil;
 
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
@@ -94,9 +92,6 @@ public abstract class PwmConstants
     public static final String PROFILE_ID_ALL = "all";
     public static final String PROFILE_ID_DEFAULT = "default";
 
-    public static final DomainID DOMAIN_ID_DEFAULT = DomainID.create( "default" );
-    public static final DomainID DOMAIN_ID_PLACEHOLDER = DomainID.create( "default" );
-
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
 
     public static final String HTTP_BASIC_AUTH_PREFIX = readPwmConstantsBundle( "httpHeaderAuthorizationBasic" );
@@ -111,27 +106,11 @@ public abstract class PwmConstants
 
     public static final String REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
+    public static final String REQUEST_ATTR_DOMAIN = "domain";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
 
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
 
-    public static final Collection<Locale> INCLUDED_LOCALES = Collections.emptyList();
-
-    static
-    {
-        /*
-        final List<Locale> localeList = new ArrayList<>();
-        final String inputString = readPwmConstantsBundle( "includedLocales" );
-        final List<String> inputList = JsonUtil.deserializeStringList( inputString );
-        for ( final String localeKey : inputList )
-        {
-            localeList.add( new Locale( localeKey ) );
-        }
-        INCLUDED_LOCALES = Collections.unmodifiableCollection( localeList );
-
-         */
-    }
-
     public static final String URL_JSP_CONFIG_GUIDE = "WEB-INF/jsp/configguide-%1%.jsp";
 
     public static final String URL_PREFIX_PRIVATE = "/private";
@@ -153,7 +132,6 @@ 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";

+ 31 - 20
server/src/main/java/password/pwm/PwmDomain.java

@@ -27,7 +27,6 @@ import password.pwm.bean.DomainID;
 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.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
@@ -40,20 +39,25 @@ 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.intruder.IntruderService;
 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.stats.StatisticsManager;
 import password.pwm.svc.token.TokenService;
-import password.pwm.svc.wordlist.SharedHistoryManager;
+import password.pwm.svc.userhistory.UserHistoryService;
+import password.pwm.svc.wordlist.SharedHistoryService;
+import password.pwm.util.DailySummaryJob;
+import password.pwm.util.PwmScheduler;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.CrService;
 import password.pwm.util.operations.OtpService;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.ExecutorService;
 
 /**
  * A repository for objects common to the servlet context.  A singleton
@@ -82,7 +86,17 @@ public class PwmDomain
             throws PwmUnrecoverableException
 
     {
+        final Instant startTime = Instant.now();
+        LOGGER.trace( () -> "initializing domain " + domainID.stringValue() );
         pwmServiceManager.initAllServices();
+
+        {
+            final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( getPwmApplication(), DailySummaryJob.class );
+            pwmApplication.getPwmScheduler().scheduleDailyZuluZeroStartJob( new DailySummaryJob( this ), executorService, TimeDuration.ZERO );
+            new DailySummaryJob( this ).run();
+        }
+
+        LOGGER.trace( () -> "completed initializing domain " + domainID.stringValue(), () -> TimeDuration.fromCurrent( startTime ) );
     }
 
     public DomainConfig getConfig( )
@@ -94,7 +108,7 @@ public class PwmDomain
     {
         return pwmApplication.getApplicationMode();
     }
-    
+
     public StatisticsManager getStatisticsManager( )
     {
         return pwmApplication.getStatisticsManager();
@@ -167,6 +181,7 @@ public class PwmDomain
     public ChaiProvider getProxyChaiProvider( final String identifier )
             throws PwmUnrecoverableException
     {
+        Objects.requireNonNull( identifier );
         return getLdapConnectionService().getProxyChaiProvider( identifier );
     }
 
@@ -185,9 +200,9 @@ public class PwmDomain
         return pwmApplication.getHttpClientService();
     }
 
-    public IntruderManager getIntruderManager()
+    public IntruderService getIntruderManager()
     {
-        return pwmApplication.getIntruderManager();
+        return pwmApplication.getIntruderService();
     }
 
     public TokenService getTokenService()
@@ -195,21 +210,11 @@ public class PwmDomain
         return pwmApplication.getTokenService();
     }
 
-    public SharedHistoryManager getSharedHistoryManager()
+    public SharedHistoryService getSharedHistoryManager()
     {
         return pwmApplication.getSharedHistoryManager();
     }
 
-    public ErrorInformation getLastLocalDBFailure()
-    {
-        return pwmApplication.getLastLocalDBFailure();
-    }
-
-    public ReportService getReportService()
-    {
-        return pwmApplication.getReportService();
-    }
-
     public PeopleSearchService getPeopleSearchService( )
     {
         return ( PeopleSearchService ) pwmServiceManager.getService( PwmServiceEnum.PeopleSearchService );
@@ -220,13 +225,19 @@ public class PwmDomain
         return pwmApplication.getPwNotifyService();
     }
 
-    public ResourceServletService getResourceServletService()
+    public ResourceServletService getResourceServletService( )
+    {
+        return ( ResourceServletService ) pwmServiceManager.getService( PwmServiceEnum.ResourceServletService );
+    }
+
+    public UserHistoryService getUserHistoryService()
     {
-        return pwmApplication.getResourceServletService();
+        return ( UserHistoryService ) pwmServiceManager.getService( PwmServiceEnum.UserHistoryService );
     }
 
     public void shutdown()
     {
+        LOGGER.trace( () -> "beginning shutdown domain " + domainID.stringValue() );
         pwmServiceManager.shutdownAllServices();
     }
 

+ 18 - 15
server/src/main/java/password/pwm/PwmEnvironment.java

@@ -28,8 +28,10 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.ContextManager;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.File;
@@ -40,6 +42,7 @@ import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
 
@@ -168,7 +171,7 @@ public class PwmEnvironment
     }
 
     public PwmEnvironment makeRuntimeInstance(
-            final AppConfig domainConfig
+            final AppConfig appConfig
     )
             throws PwmUnrecoverableException
     {
@@ -176,7 +179,7 @@ public class PwmEnvironment
                 .applicationMode( PwmApplicationMode.READ_ONLY )
                 .internalRuntimeInstance( true )
                 .configurationFile( null )
-                .config( domainConfig )
+                .config( appConfig )
                 .build();
     }
 
@@ -235,47 +238,47 @@ public class PwmEnvironment
     {
         public static Set<ApplicationFlag> readApplicationFlagsFromSystem( final String contextName )
         {
-            final String rawValue = readValueFromSystem( EnvironmentParameter.applicationFlags, contextName );
-            if ( rawValue != null )
+            final Optional<String> rawValue = readValueFromSystem( EnvironmentParameter.applicationFlags, contextName );
+            if ( rawValue.isPresent() )
             {
-                return parseApplicationFlagValueParameter( rawValue );
+                return parseApplicationFlagValueParameter( rawValue.get() );
             }
             return Collections.emptySet();
         }
 
         public static Map<ApplicationParameter, String> readApplicationParmsFromSystem( final String contextName )
         {
-            final String rawValue = readValueFromSystem( EnvironmentParameter.applicationParamFile, contextName );
-            if ( rawValue != null )
+            final Optional<String> rawValue = readValueFromSystem( EnvironmentParameter.applicationParamFile, contextName );
+            if ( rawValue.isPresent() )
             {
-                return readAppParametersFromPath( rawValue );
+                return readAppParametersFromPath( rawValue.get() );
             }
             return Collections.emptyMap();
         }
 
-        public static String readValueFromSystem( final PwmEnvironment.EnvironmentParameter parameter, final String contextName )
+        public static Optional<String> readValueFromSystem( final PwmEnvironment.EnvironmentParameter parameter, final String contextName )
         {
             final List<String> namePossibilities = parameter.possibleNames( contextName );
 
             for ( final String propertyName : namePossibilities )
             {
                 final String propValue = System.getProperty( propertyName );
-                if ( propValue != null && !propValue.isEmpty() )
+                if ( StringUtil.notEmpty( propValue ) )
                 {
-                    return propValue;
+                    return Optional.of( propValue );
                 }
             }
 
             for ( final String propertyName : namePossibilities )
             {
                 final String propValue = System.getenv( propertyName );
-                if ( propValue != null && !propValue.isEmpty() )
+                if ( StringUtil.notEmpty( propValue ) )
                 {
-                    return propValue;
+                    return Optional.of( propValue );
                 }
             }
 
-            return null;
+            return Optional.empty();
         }
 
         public static Set<ApplicationFlag> parseApplicationFlagValueParameter( final String input )
@@ -288,7 +291,7 @@ public class PwmEnvironment
             try
             {
                 final List<String> jsonValues = JsonUtil.deserializeStringList( input );
-                final Set<ApplicationFlag> returnFlags = JavaHelper.readEnumSetFromStringCollection( ApplicationFlag.class, jsonValues );
+                final Set<ApplicationFlag> returnFlags = CollectionUtil.readEnumSetFromStringCollection( ApplicationFlag.class, jsonValues );
                 return Collections.unmodifiableSet( returnFlags );
             }
             catch ( final Exception e )

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

@@ -24,8 +24,5 @@ import password.pwm.util.logging.PwmLogger;
 
 public class PwmEnvironmentUtils
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmEnvironmentUtils.class );
-
-
 }

+ 11 - 7
server/src/main/java/password/pwm/bean/DomainID.java

@@ -23,21 +23,23 @@ package password.pwm.bean;
 import org.jetbrains.annotations.NotNull;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingScope;
+import password.pwm.config.value.StringValue;
 import password.pwm.util.java.JavaHelper;
 
 import java.io.Serializable;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
-import java.util.regex.Pattern;
 
 public class DomainID implements Comparable<DomainID>, Serializable
 {
+    public static final List<String> DOMAIN_RESERVED_WORDS = List.of( "system", "private", "public", "pwm", "sspr", "domain", "profile", "password" );
+    public static final DomainID DOMAIN_ID_DEFAULT = create( "default" );
+    public static final DomainID DOMAIN_ID_PLACEHOLDER = create( "default" );
+
     private static final String SYSTEM_ID = "system";
     private static final DomainID SYSTEM_DOMAIN_ID = new DomainID( SYSTEM_ID );
 
-   // 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()
@@ -52,10 +54,12 @@ public class DomainID implements Comparable<DomainID>, Serializable
 
     public static DomainID create( final String domainID )
     {
-        final Pattern pattern = PATTERN;
-        if ( !pattern.matcher( domainID ).matches() )
+        Objects.requireNonNull( domainID );
+        
+        final List<String> errorMessages = StringValue.validateValue( PwmSetting.DOMAIN_LIST, domainID );
+        if ( !errorMessages.isEmpty() )
         {
-            throw new IllegalArgumentException( "domainID value '" + domainID + " ' does not match required syntax pattern" );
+            throw new IllegalArgumentException( "domainID value '" + domainID + "' does not match required syntax pattern for user defined domains: " + errorMessages.get( 0 ) );
         }
         return new DomainID( domainID );
     }

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

@@ -21,6 +21,7 @@
 package password.pwm.bean;
 
 import lombok.Value;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.secure.X509Utils;
 
@@ -41,6 +42,7 @@ public class PrivateKeyCertificate implements Serializable
     private final String privateKey;
 
     public PrivateKeyCertificate( final List<X509Certificate> certificates, final PrivateKey privateKey )
+            throws PwmUnrecoverableException
     {
         this.b64certificates = X509Utils.certificatesToBase64s( certificates );
         this.privateKey = StringUtil.base64Encode( privateKey.getEncoded() );

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

@@ -108,7 +108,7 @@ public class TokenDestinationItem implements Serializable
                 }
         )
         {
-            if ( !StringUtil.isEmpty( emailValue ) )
+            if ( StringUtil.notEmpty( emailValue ) )
             {
                 final String idHash = domainSecureService.hash( emailValue + Type.email.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
@@ -129,7 +129,7 @@ public class TokenDestinationItem implements Serializable
                 }
         )
         {
-            if ( !StringUtil.isEmpty( smsValue ) )
+            if ( StringUtil.notEmpty( smsValue ) )
             {
                 final String idHash = domainSecureService.hash( smsValue + Type.sms.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()

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

@@ -134,7 +134,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
             throws PwmUnrecoverableException
     {
         // use local cache first.
-        if ( !StringUtil.isEmpty( obfuscatedValue ) )
+        if ( StringUtil.notEmpty( obfuscatedValue ) )
         {
             return obfuscatedValue;
         }
@@ -144,7 +144,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         final CacheKey cacheKey = CacheKey.newKey( this.getClass(), this, "obfuscatedKey" );
         final String cachedValue = cacheService.get( cacheKey, String.class );
 
-        if ( !StringUtil.isEmpty( cachedValue ) )
+        if ( StringUtil.notEmpty( cachedValue ) )
         {
             obfuscatedValue = cachedValue;
             return cachedValue;
@@ -172,7 +172,9 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
 
     public String toDisplayString( )
     {
-        return this.getUserDN() + ( ( this.getLdapProfileID() != null && !this.getLdapProfileID().isEmpty() ) ? " (" + this.getLdapProfileID() + ")" : "" );
+        return "[" + this.getDomainID() + "]"
+                + " " + this.getUserDN()
+                + ( ( this.getLdapProfileID() != null && !this.getLdapProfileID().isEmpty() ) ? " (" + this.getLdapProfileID() + ")" : "" );
     }
 
     public static UserIdentity fromObfuscatedKey( final String key, final PwmApplication pwmApplication )
@@ -314,7 +316,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
             return this;
         }
 
-        final ChaiUser chaiUser = pwmApplication.getDomains().get( this.getDomainID() ).getProxiedChaiUser( this );
+        final ChaiUser chaiUser = pwmApplication.domains().get( this.getDomainID() ).getProxiedChaiUser( this );
         final String userDN;
         try
         {

+ 42 - 17
server/src/main/java/password/pwm/config/AppConfig.java

@@ -29,9 +29,11 @@ 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.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.LazySupplier;
@@ -50,28 +52,30 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
-public class AppConfig
+public class AppConfig implements SettingReader
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( AppConfig.class );
     private final ConfigurationSuppliers configurationSuppliers = new ConfigurationSuppliers();
 
     private final StoredConfiguration storedConfiguration;
-    private final SettingReader settingReader;
+    private final StoredSettingReader settingReader;
     private final Map<DomainID, DomainConfig> domainConfigMap;
-    private final List<String> domainIDList;
+    private final Set<String> domainIDList;
 
     private PwmSecurityKey tempInstanceKey = null;
 
     public AppConfig( final StoredConfiguration storedConfiguration )
     {
         this.storedConfiguration = storedConfiguration;
-        this.settingReader = new SettingReader( storedConfiguration, null, DomainID.systemId() );
+        this.settingReader = new StoredSettingReader( storedConfiguration, null, DomainID.systemId() );
 
-        this.domainIDList = settingReader.readSettingAsStringArray( PwmSetting.DOMAIN_LIST ).stream()
-                .collect( Collectors.toUnmodifiableList() );
+        this.domainIDList = Collections.unmodifiableSet( new TreeSet<>( settingReader.readSettingAsStringArray( PwmSetting.DOMAIN_LIST ).stream()
+                .sorted()
+                .collect( Collectors.toList() ) ) );
 
         this.domainConfigMap = domainIDList.stream()
                 .collect( Collectors.toUnmodifiableMap(
@@ -79,7 +83,7 @@ public class AppConfig
                         ( domainID ) -> new DomainConfig( this, DomainID.create( domainID ) ) ) );
     }
 
-    public List<String> getDomainIDs()
+    public Set<String> getDomainIDs()
     {
         return domainIDList;
     }
@@ -89,11 +93,6 @@ public class AppConfig
         return domainConfigMap;
     }
 
-    public DomainConfig getDefaultDomainConfig()
-    {
-        return domainConfigMap.get( PwmConstants.DOMAIN_ID_PLACEHOLDER );
-    }
-
     public String readSettingAsString( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsString( pwmSetting );
@@ -129,31 +128,35 @@ public class AppConfig
         return configurationSuppliers.pwmSecurityKey.call();
     }
 
+    @Override
     public <E extends Enum<E>> Set<E> readSettingAsOptionList( final PwmSetting pwmSetting, final Class<E> enumClass )
     {
         return settingReader.readSettingAsOptionList( pwmSetting, enumClass );
     }
 
+    @Override
     public boolean readSettingAsBoolean( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsBoolean( pwmSetting );
     }
 
+    @Override
     public List<String> readSettingAsStringArray( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsStringArray( pwmSetting );
     }
 
-    public PwmLogLevel getEventLogLocalDBLevel( )
+    public PwmLogLevel getEventLogLocalDBLevel()
     {
         return readSettingAsEnum( PwmSetting.EVENTS_LOCALDB_LOG_LEVEL, PwmLogLevel.class );
     }
 
-    public boolean isDevDebugMode( )
+    public boolean isDevDebugMode()
     {
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
     }
 
+    @Override
     public long readSettingAsLong( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsLong( pwmSetting );
@@ -169,26 +172,42 @@ public class AppConfig
         return List.copyOf( configurationSuppliers.localeFlagMap.get().keySet() );
     }
 
+    @Override
     public PrivateKeyCertificate readSettingAsPrivateKey( final PwmSetting setting )
     {
         return settingReader.readSettingAsPrivateKey( setting );
     }
 
+    @Override
     public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
     {
         return settingReader.readSettingAsEnum( setting, enumClass );
     }
 
+    @Override
     public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsFile( pwmSetting );
     }
 
+    @Override
     public List<X509Certificate> readSettingAsCertificate( final PwmSetting pwmSetting )
     {
         return settingReader.readSettingAsCertificate( pwmSetting );
     }
 
+    @Override
+    public List<UserPermission> readSettingAsUserPermission( final PwmSetting pwmSetting )
+    {
+        return settingReader.readSettingAsUserPermission( pwmSetting );
+    }
+
+    @Override
+    public Map<Locale, String> readLocalizedBundle( final PwmLocaleBundle className, final String keyName )
+    {
+        return settingReader.readLocalizedBundle( className, keyName );
+    }
+
     private class ConfigurationSuppliers
     {
         private final Supplier<Map<String, String>> appPropertyOverrides = new LazySupplier<>( () ->
@@ -294,14 +313,20 @@ public class AppConfig
 
     public boolean hasDbConfigured( )
     {
-        return !StringUtil.isEmpty( readSettingAsString( PwmSetting.DATABASE_CLASS ) )
-                && !StringUtil.isEmpty( readSettingAsString( PwmSetting.DATABASE_URL ) )
-                && !StringUtil.isEmpty( readSettingAsString( PwmSetting.DATABASE_USERNAME ) )
+        return StringUtil.notEmpty( readSettingAsString( PwmSetting.DATABASE_CLASS ) )
+                && StringUtil.notEmpty( readSettingAsString( PwmSetting.DATABASE_URL ) )
+                && StringUtil.notEmpty( readSettingAsString( PwmSetting.DATABASE_USERNAME ) )
                 && readSettingAsPassword( PwmSetting.DATABASE_PASSWORD ) != null;
     }
 
+    @Override
     public PasswordData readSettingAsPassword( final PwmSetting setting )
     {
         return settingReader.readSettingAsPassword( setting );
     }
+
+    public boolean isMultiDomain()
+    {
+        return this.getDomainConfigs().size() > 1;
+    }
 }

+ 17 - 29
server/src/main/java/password/pwm/config/DomainConfig.java

@@ -52,7 +52,6 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.logging.PwmLogger;
@@ -75,7 +74,7 @@ import java.util.stream.Collectors;
 /**
  * @author Jason D. Rivard
  */
-public class DomainConfig
+public class DomainConfig implements SettingReader
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( DomainConfig.class );
 
@@ -86,14 +85,14 @@ public class DomainConfig
     private final ConfigurationSuppliers configurationSuppliers = new ConfigurationSuppliers();
 
     private final DataCache dataCache = new DataCache();
-    private final SettingReader settingReader;
+    private final StoredSettingReader settingReader;
 
     public DomainConfig( final AppConfig appConfig, final DomainID domainID )
     {
         this.appConfig = Objects.requireNonNull( appConfig );
         this.storedConfiguration = appConfig.getStoredConfiguration();
         this.domainID = Objects.requireNonNull( domainID );
-        this.settingReader = new SettingReader( storedConfiguration, null, domainID );
+        this.settingReader = new StoredSettingReader( storedConfiguration, null, domainID );
     }
 
     public AppConfig getAppConfig()
@@ -101,6 +100,12 @@ public class DomainConfig
         return appConfig;
     }
 
+    public boolean isAdministrativeDomain()
+    {
+        final String adminDomainStr = getAppConfig().readSettingAsString( PwmSetting.DOMAIN_SYSTEM_ADMIN );
+        return getDomainID().stringValue().equals( adminDomainStr );
+    }
+
 
     public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
     {
@@ -164,28 +169,7 @@ public class DomainConfig
 
     public Map<Locale, String> readLocalizedBundle( final PwmLocaleBundle className, final String keyName )
     {
-        final String key = className + "-" + keyName;
-        if ( dataCache.customText.containsKey( key ) )
-        {
-            return dataCache.customText.get( key );
-        }
-
-        final Map<String, String> storedValue = storedConfiguration.readLocaleBundleMap( className, keyName );
-        if ( storedValue == null || storedValue.isEmpty() )
-        {
-            dataCache.customText.put( key, null );
-            return null;
-        }
-
-        final Map<Locale, String> localizedMap = new LinkedHashMap<>();
-        for ( final Map.Entry<String, String> entry : storedValue.entrySet() )
-        {
-            final String localeKey = entry.getKey();
-            localizedMap.put( LocaleHelper.parseLocaleString( localeKey ), entry.getValue() );
-        }
-
-        dataCache.customText.put( key, localizedMap );
-        return localizedMap;
+        return settingReader.readLocalizedBundle( className, keyName );
     }
 
     public List<String> getChallengeProfileIDs( )
@@ -289,7 +273,7 @@ public class DomainConfig
 
     public PwmSettingTemplateSet getTemplate( )
     {
-        return storedConfiguration.getTemplateSet();
+        return storedConfiguration.getTemplateSet().get( domainID );
     }
 
     public String readAppProperty( final AppProperty property )
@@ -348,7 +332,7 @@ public class DomainConfig
                 }
                 catch ( final Exception e )
                 {
-                    final String errorMsg = "unexpected error generating Security Key crypto: " + e.getMessage();
+                    final String errorMsg = "unexpected err0or generating Security Key crypto: " + e.getMessage();
                     final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
                     LOGGER.error( errorInfo::toDebugStr, e );
                     throw new PwmUnrecoverableException( errorInfo );
@@ -360,7 +344,6 @@ public class DomainConfig
     private static class DataCache
     {
         private final Map<String, PwmPasswordPolicy> cachedPasswordPolicy = new LinkedHashMap<>();
-        private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
     }
 
     /* generic profile stuff */
@@ -424,4 +407,9 @@ public class DomainConfig
         }
         return Optional.empty();
     }
+
+    public String getDisplayName( final Locale locale )
+    {
+        return getDomainID().toString();
+    }
 }

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

@@ -23,7 +23,7 @@ package password.pwm.config;
 import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.config.value.StoredValue;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 
 import java.util.Arrays;
@@ -58,7 +58,9 @@ public enum PwmSetting
 
     // domains
     DOMAIN_LIST(
-            "domain.list", PwmSettingSyntax.DOMAINS, PwmSettingCategory.DOMAINS ),
+            "domain.list", PwmSettingSyntax.DOMAIN, PwmSettingCategory.DOMAINS ),
+    DOMAIN_SYSTEM_ADMIN(
+            "domain.system.adminDomain", PwmSettingSyntax.STRING, PwmSettingCategory.DOMAINS ),
 
     // application settings
     APP_PROPERTY_OVERRIDES(
@@ -329,9 +331,11 @@ public enum PwmSetting
             "email.queueMaxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_ADVANCED_SETTINGS(
             "email.smtp.advancedSettings", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.EMAIL_SETTINGS ),
+    EMAIL_SYSTEM_FROM_ADDRESS(
+            "email.system.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
 
     // email template
-    EMAIL_DEFAULT_FROM_ADDRESS(
+    EMAIL_DOMAIN_FROM_ADDRESS(
             "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_TEMPLATES ),
     EMAIL_CHANGEPASSWORD(
             "email.changePassword", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
@@ -1161,7 +1165,7 @@ public enum PwmSetting
     REPORTING_JOB_TIME_OFFSET(
             "reporting.job.timeOffset", PwmSettingSyntax.DURATION, PwmSettingCategory.REPORTING ),
     REPORTING_USER_MATCH(
-            "reporting.ldap.userMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REPORTING ),
+            "reporting.ldap.userMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.UI_FEATURES ),
     REPORTING_MAX_QUERY_SIZE(
             "reporting.ldap.maxQuerySize", PwmSettingSyntax.NUMERIC, PwmSettingCategory.REPORTING ),
     REPORTING_JOB_INTENSITY(
@@ -1464,7 +1468,7 @@ public enum PwmSetting
             {
                 for ( final TemplateSetReference<T> templateSetReference : templateSetReferences )
                 {
-                    final Set<PwmSettingTemplate> temporarySet = JavaHelper.copiedEnumSet( templateSetReference.getSettingTemplates(), PwmSettingTemplate.class );
+                    final Set<PwmSettingTemplate> temporarySet = CollectionUtil.copiedEnumSet( templateSetReference.getSettingTemplates(), PwmSettingTemplate.class );
                     temporarySet.retainAll( pwmSettingTemplate.getTemplates() );
                     final int matchCount = temporarySet.size();
                     if ( matchCount == matchCountExamSize )

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

@@ -23,6 +23,7 @@ package password.pwm.config;
 import password.pwm.PwmConstants;
 import password.pwm.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.XmlElement;
@@ -506,7 +507,7 @@ public enum PwmSettingCategory
             final Set<PwmSettingCategory> categories = Arrays.stream( PwmSettingCategory.values() )
                     .filter( ( loopCategory ) -> loopCategory.getParent() == category )
                     .collect( Collectors.toUnmodifiableSet() );
-            return Collections.unmodifiableSet( JavaHelper.copiedEnumSet( categories, PwmSettingCategory.class ) );
+            return Collections.unmodifiableSet( CollectionUtil.copiedEnumSet( categories, PwmSettingCategory.class ) );
         }
 
         public static Set<PwmSetting> readSettings( final PwmSettingCategory category )
@@ -514,7 +515,7 @@ public enum PwmSettingCategory
             final Set<PwmSetting> settings = Arrays.stream( PwmSetting.values() )
                     .filter( ( setting ) -> setting.getCategory() == category )
                     .collect( Collectors.toSet() );
-            return Collections.unmodifiableSet( JavaHelper.copiedEnumSet( settings, PwmSetting.class ) );
+            return Collections.unmodifiableSet( CollectionUtil.copiedEnumSet( settings, PwmSetting.class ) );
         }
     }
 }

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

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

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

@@ -29,10 +29,12 @@ import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -165,6 +167,39 @@ public class PwmSettingMetaDataReader
         }
     }
 
+    public static void initCache()
+    {
+        final Instant startTime = Instant.now();
+        for ( final PwmSetting pwmSetting : PwmSetting.values() )
+        {
+            pwmSetting.getProperties();
+            pwmSetting.getFlags();
+            pwmSetting.getOptions();
+            pwmSetting.getLabel( PwmConstants.DEFAULT_LOCALE );
+            pwmSetting.getDescription( PwmConstants.DEFAULT_LOCALE );
+            pwmSetting.getExample( PwmSettingTemplateSet.getDefault() );
+            pwmSetting.isRequired();
+            pwmSetting.isHidden();
+            pwmSetting.getLevel();
+            pwmSetting.getRegExPattern();
+            pwmSetting.getLDAPPermissionInfo();
+            pwmSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
+        }
+        for ( final PwmSettingCategory pwmSettingCategory : PwmSettingCategory.values() )
+        {
+            pwmSettingCategory.getLabel( PwmConstants.DEFAULT_LOCALE );
+            pwmSettingCategory.getDescription( PwmConstants.DEFAULT_LOCALE );
+            pwmSettingCategory.isHidden();
+            pwmSettingCategory.getLevel();
+            pwmSettingCategory.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
+            pwmSettingCategory.getScope();
+            pwmSettingCategory.getChildren();
+            pwmSettingCategory.getLevel();
+            pwmSettingCategory.getSettings();
+        }
+        LOGGER.trace( () -> "completed PwmSetting xml cache initialization", () -> TimeDuration.fromCurrent( startTime ) );
+    }
+
     private static class InternalReader
     {
         private static Set<PwmSettingFlag> readFlags( final PwmSetting pwmSetting )
@@ -209,8 +244,7 @@ public class PwmSettingMetaDataReader
                     }
                 }
             }
-            final Map<String, String> finalList = Collections.unmodifiableMap( returnList );
-            return Collections.unmodifiableMap( finalList );
+            return Collections.unmodifiableMap( returnList );
         }
 
         private static Collection<LDAPPermissionInfo> readLdapPermissionInfo( final PwmSetting pwmSetting )

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

@@ -71,7 +71,7 @@ public enum PwmSettingSyntax
     OPTIONLIST( OptionListValue.factory() ),
     FILE( FileValue.factory() ),
     PROFILE( StringArrayValue.factory() ),
-    DOMAINS( StringArrayValue.factory() ),
+    DOMAIN( StringArrayValue.factory() ),
     VERIFICATION_METHOD( VerificationMethodValue.factory() ),
     PRIVATE_KEY( PrivateKeyValue.factory() ),
     NAMED_SECRET( NamedSecretValue.factory() ),

+ 20 - 26
server/src/main/java/password/pwm/config/PwmSettingTemplateSet.java

@@ -20,47 +20,36 @@
 
 package password.pwm.config;
 
+import lombok.Value;
+import password.pwm.util.java.CollectionUtil;
+
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
+@Value
 public class PwmSettingTemplateSet implements Serializable
 {
     private final Set<PwmSettingTemplate> templates;
 
     public PwmSettingTemplateSet( final Set<PwmSettingTemplate> templates )
     {
-        final Set<PwmSettingTemplate> workingSet = EnumSet.noneOf( PwmSettingTemplate.class );
+        final Set<PwmSettingTemplate> workingSet = CollectionUtil.copiedEnumSet( templates, PwmSettingTemplate.class );
 
-        if ( templates != null )
-        {
-            for ( final PwmSettingTemplate template : templates )
-            {
-                if ( template != null )
-                {
-                    workingSet.add( template );
-                }
-            }
-        }
+        final Set<PwmSettingTemplate.Type> seenTypes = workingSet.stream()
+                .map( PwmSettingTemplate::getType )
+                .collect( Collectors.toSet() );
 
-        final Set<PwmSettingTemplate.Type> seenTypes = EnumSet.noneOf( PwmSettingTemplate.Type.class );
-        for ( final PwmSettingTemplate template : workingSet )
-        {
-            seenTypes.add( template.getType() );
-        }
+        workingSet.addAll( Arrays.stream( PwmSettingTemplate.Type.values() )
+                .filter( type -> !seenTypes.contains( type ) )
+                .map( PwmSettingTemplate.Type::getDefaultValue )
+                .collect( Collectors.toSet( ) ) );
 
-        for ( final PwmSettingTemplate.Type type : PwmSettingTemplate.Type.values() )
-        {
-            if ( !seenTypes.contains( type ) )
-            {
-                workingSet.add( type.getDefaultValue() );
-            }
-        }
-
-        this.templates = Collections.unmodifiableSet( workingSet );
+        this.templates = Set.copyOf( workingSet );
     }
 
     public Set<PwmSettingTemplate> getTemplates( )
@@ -73,6 +62,11 @@ public class PwmSettingTemplateSet implements Serializable
         return new PwmSettingTemplateSet( null );
     }
 
+    public boolean contains( final PwmSettingTemplate template )
+    {
+        return templates.contains( template );
+    }
+
     /**
      * Get all possible templateSets, useful for testing.
      * @return A list of all possible template sets.

+ 14 - 263
server/src/main/java/password/pwm/config/SettingReader.java

@@ -20,291 +20,42 @@
 
 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.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.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
-import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
-import java.lang.reflect.InvocationTargetException;
 import java.security.cert.X509Certificate;
-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.Set;
-import java.util.stream.Collectors;
 
-public class SettingReader
+public interface SettingReader
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( SettingReader.class );
+    <E extends Enum<E>> Set<E> readSettingAsOptionList( PwmSetting pwmSetting, Class<E> enumClass );
 
-    private final ProfileReader profileReader = new ProfileReader();
+    boolean readSettingAsBoolean( PwmSetting pwmSetting );
 
-    private final StoredConfiguration storedConfiguration;
-    private final String profileID;
-    private final DomainID domainID;
+    List<String> readSettingAsStringArray( PwmSetting pwmSetting );
 
-    public SettingReader( final StoredConfiguration storedConfiguration, final String profileID, final DomainID domainID )
-    {
-        this.storedConfiguration = Objects.requireNonNull( storedConfiguration );
-        this.profileID = profileID;
-        this.domainID = Objects.requireNonNull( domainID );
-    }
+    long readSettingAsLong( PwmSetting pwmSetting );
 
-    public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToUserPermissions( readSetting( setting ) );
-    }
+    PrivateKeyCertificate readSettingAsPrivateKey( PwmSetting setting );
 
-    public String readSettingAsString( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToString( readSetting( setting ) );
-    }
+    <E extends Enum<E>> E readSettingAsEnum( PwmSetting setting, Class<E> enumClass );
 
-    public List<String> readSettingAsStringArray( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToStringArray( readSetting( setting ) );
-    }
+    Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( PwmSetting pwmSetting );
 
-    public List<String> readSettingAsLocalizedStringArray( final PwmSetting setting, final Locale locale )
-    {
-        return ValueTypeConverter.valueToLocalizedStringArray( readSetting( setting ), locale );
-    }
+    List<X509Certificate> readSettingAsCertificate( PwmSetting pwmSetting );
 
-    public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting pwmSetting )
-    {
-        return ValueTypeConverter.valueToFile( pwmSetting, readSetting( pwmSetting ) );
-    }
+    List<UserPermission> readSettingAsUserPermission( PwmSetting 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
-                ) );
+    String readSettingAsString( PwmSetting setting );
 
-        final Locale matchedLocale = LocaleHelper.localeResolver( locale, availableLocaleMap.keySet() );
+    PasswordData readSettingAsPassword( PwmSetting setting );
 
-        return availableLocaleMap.get( matchedLocale );
-    }
+    Map<Locale, String> readLocalizedBundle( PwmLocaleBundle className, String keyName );
 
-    public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToForm( readSetting( setting ) );
-    }
-
-    public <E extends Enum<E>> Set<E> readSettingAsOptionList( final PwmSetting setting, final Class<E> enumClass )
-    {
-        return ValueTypeConverter.valueToOptionList( setting, readSetting( setting ), enumClass );
-    }
-
-    public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
-    {
-        return ValueTypeConverter.valueToEnum( setting, readSetting( setting ), enumClass );
-    }
-
-    public List<ActionConfiguration> readSettingAsAction( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToAction( setting, readSetting( setting ) );
-    }
-
-    public List<X509Certificate> readSettingAsCertificate( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToX509Certificates( setting, readSetting( setting ) );
-    }
-
-    public boolean readSettingAsBoolean( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToBoolean( readSetting( setting ) );
-    }
-
-    public long readSettingAsLong( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToLong( readSetting( setting ) );
-    }
-
-    public String readSettingAsLocalizedString( final PwmSetting setting, final Locale locale )
-    {
-        return ValueTypeConverter.valueToLocalizedString( readSetting( setting ), locale );
-    }
-
-    public PasswordData readSettingAsPassword( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToPassword( readSetting( setting ) );
-    }
-
-    public List<RemoteWebServiceConfiguration> readSettingAsRemoteWebService( final PwmSetting pwmSetting )
-    {
-        return ValueTypeConverter.valueToRemoteWebServiceConfiguration( readSetting( pwmSetting ) );
-    }
-
-    public Map<String, NamedSecretData> readSettingAsNamedPasswords( final PwmSetting setting )
-    {
-        return ValueTypeConverter.valueToNamedPassword( readSetting( setting ) );
-    }
-
-    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() );
-        }
-
-        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, final DomainID domainID )
-    {
-        return profileReader.getProfileMap( profileDefinition, domainID );
-    }
-
-
-    private class ProfileReader
-    {
-        private final Map<ProfileDefinition, Map> profileCache = new LinkedHashMap<>();
-
-        private <T extends Profile> Map<String, T> getProfileMap( final ProfileDefinition profileDefinition, final DomainID domainID )
-        {
-            return profileCache.computeIfAbsent( profileDefinition, ( p ) ->
-            {
-                final Map<String, T> returnMap = new LinkedHashMap<>();
-                final Map<String, Profile> profileMap = profileMap( profileDefinition, domainID );
-                for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
-                {
-                    returnMap.put( entry.getKey(), ( T ) entry.getValue() );
-                }
-                return Collections.unmodifiableMap( returnMap );
-            } );
-        }
-
-        private Map<String, Profile> profileMap( final ProfileDefinition profileDefinition, final DomainID domainID )
-        {
-            if ( profileDefinition.getProfileFactoryClass().isEmpty() )
-            {
-                return Collections.emptyMap();
-            }
-
-            return profileIDsForCategory( profileDefinition.getCategory() ).stream()
-                    .collect( Collectors.toUnmodifiableMap(
-                        profileID -> profileID,
-                        profileID -> newProfileForID( profileDefinition, domainID, profileID )
-                    ) );
-        }
-
-        private Profile newProfileForID( final ProfileDefinition profileDefinition, final DomainID domainID, final String profileID )
-        {
-            Objects.requireNonNull( profileDefinition );
-            Objects.requireNonNull( profileID );
-
-            final Optional<Class<? extends Profile.ProfileFactory>> optionalProfileFactoryClass = profileDefinition.getProfileFactoryClass();
-
-            if ( optionalProfileFactoryClass.isPresent() )
-            {
-                final Profile.ProfileFactory profileFactory;
-                try
-                {
-                    profileFactory = optionalProfileFactoryClass.get().getDeclaredConstructor().newInstance();
-                    return profileFactory.makeFromStoredConfiguration( storedConfiguration, domainID, profileID );
-                }
-                catch ( final InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e )
-                {
-                    throw new IllegalStateException( "unable to create profile instance for " + profileDefinition );
-                }
-            }
-
-            throw new IllegalStateException( "unable to create profile instance for " + profileDefinition + " ( profile factory class not defined )" );
-        }
-
-        public List<String> profileIDsForCategory( final PwmSettingCategory pwmSettingCategory )
-        {
-            final PwmSetting profileSetting = pwmSettingCategory.getProfileSetting().orElseThrow( IllegalStateException::new );
-            return SettingReader.this.readSettingAsStringArray( profileSetting );
-        }
-    }
-
-    private StoredValue readSetting( final PwmSetting setting )
-    {
-        if ( DomainID.systemId().equals( domainID ) )
-        {
-            if ( setting.getCategory().getScope() == PwmSettingScope.DOMAIN )
-            {
-                final String msg = "attempt to read system scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' as system scope";
-                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
-                throw new IllegalStateException( msg, pwmUnrecoverableException );
-            }
-        }
-        else
-        {
-            if ( setting.getCategory().getScope() == PwmSettingScope.SYSTEM )
-            {
-                final String msg = "attempt to read system scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' as domain scope";
-                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
-                throw new IllegalStateException( msg, pwmUnrecoverableException );
-            }
-        }
-
-        if ( setting.getFlags().contains( PwmSettingFlag.Deprecated ) )
-        {
-            LOGGER.warn( () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
-        }
-
-        if ( StringUtil.isEmpty( profileID ) )
-        {
-            if ( setting.getCategory().hasProfiles() )
-            {
-                throw new IllegalStateException( "attempt to read profiled setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via non-profile" );
-            }
-        }
-        else
-        {
-            if ( !setting.getCategory().hasProfiles() )
-            {
-                throw new IllegalStateException( "attempt to read non-profiled setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via profile" );
-            }
-        }
-
-        final StoredConfigKey key = StoredConfigKey.forSetting( setting, profileID, domainID );
-        return StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
-    }
 }

+ 345 - 0
server/src/main/java/password/pwm/config/StoredSettingReader.java

@@ -0,0 +1,345 @@
+/*
+ * 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;
+
+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.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.i18n.PwmLocaleBundle;
+import password.pwm.util.PasswordData;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import java.lang.reflect.InvocationTargetException;
+import java.security.cert.X509Certificate;
+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.Set;
+import java.util.stream.Collectors;
+
+public class StoredSettingReader implements SettingReader
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredSettingReader.class );
+
+    private final ProfileReader profileReader = new ProfileReader();
+
+    private final StoredConfiguration storedConfiguration;
+    private final String profileID;
+    private final DomainID domainID;
+    private final DataCache dataCache = new DataCache();
+
+    public StoredSettingReader( final StoredConfiguration storedConfiguration, final String profileID, final DomainID domainID )
+    {
+        this.storedConfiguration = Objects.requireNonNull( storedConfiguration );
+        this.profileID = profileID;
+        this.domainID = Objects.requireNonNull( domainID );
+    }
+
+    private static class DataCache
+    {
+        private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
+    }
+
+    @Override
+    public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToUserPermissions( readSetting( setting ) );
+    }
+
+    @Override
+    public String readSettingAsString( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToString( readSetting( setting ) );
+    }
+
+    public List<String> readSettingAsStringArray( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToStringArray( readSetting( setting ) );
+    }
+
+    public List<String> readSettingAsLocalizedStringArray( final PwmSetting setting, final Locale locale )
+    {
+        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 ) );
+    }
+
+    public <E extends Enum<E>> Set<E> readSettingAsOptionList( final PwmSetting setting, final Class<E> enumClass )
+    {
+        return ValueTypeConverter.valueToOptionList( setting, readSetting( setting ), enumClass );
+    }
+
+    public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
+    {
+        return ValueTypeConverter.valueToEnum( setting, readSetting( setting ), enumClass );
+    }
+
+    public List<ActionConfiguration> readSettingAsAction( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToAction( setting, readSetting( setting ) );
+    }
+
+    public List<X509Certificate> readSettingAsCertificate( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToX509Certificates( setting, readSetting( setting ) );
+    }
+
+    public boolean readSettingAsBoolean( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToBoolean( readSetting( setting ) );
+    }
+
+    public long readSettingAsLong( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToLong( readSetting( setting ) );
+    }
+
+    public String readSettingAsLocalizedString( final PwmSetting setting, final Locale locale )
+    {
+        return ValueTypeConverter.valueToLocalizedString( readSetting( setting ), locale );
+    }
+
+    public PasswordData readSettingAsPassword( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToPassword( readSetting( setting ) );
+    }
+
+    public List<RemoteWebServiceConfiguration> readSettingAsRemoteWebService( final PwmSetting pwmSetting )
+    {
+        return ValueTypeConverter.valueToRemoteWebServiceConfiguration( readSetting( pwmSetting ) );
+    }
+
+    public Map<String, NamedSecretData> readSettingAsNamedPasswords( final PwmSetting setting )
+    {
+        return ValueTypeConverter.valueToNamedPassword( readSetting( setting ) );
+    }
+
+    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() );
+        }
+
+        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, final DomainID domainID )
+    {
+        return profileReader.getProfileMap( profileDefinition, domainID );
+    }
+
+
+    private class ProfileReader
+    {
+        private final Map<ProfileDefinition, Map> profileCache = new LinkedHashMap<>();
+
+        private <T extends Profile> Map<String, T> getProfileMap( final ProfileDefinition profileDefinition, final DomainID domainID )
+        {
+            return profileCache.computeIfAbsent( profileDefinition, ( p ) ->
+            {
+                final Map<String, T> returnMap = new LinkedHashMap<>();
+                final Map<String, Profile> profileMap = profileMap( profileDefinition, domainID );
+                for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
+                {
+                    returnMap.put( entry.getKey(), ( T ) entry.getValue() );
+                }
+                return Collections.unmodifiableMap( returnMap );
+            } );
+        }
+
+        private Map<String, Profile> profileMap( final ProfileDefinition profileDefinition, final DomainID domainID )
+        {
+            if ( profileDefinition.getProfileFactoryClass().isEmpty() )
+            {
+                return Collections.emptyMap();
+            }
+
+            return profileIDsForCategory( profileDefinition.getCategory() ).stream()
+                    .collect( Collectors.toUnmodifiableMap(
+                        profileID -> profileID,
+                        profileID -> newProfileForID( profileDefinition, domainID, profileID )
+                    ) );
+        }
+
+        private Profile newProfileForID( final ProfileDefinition profileDefinition, final DomainID domainID, final String profileID )
+        {
+            Objects.requireNonNull( profileDefinition );
+            Objects.requireNonNull( profileID );
+
+            final Optional<Class<? extends Profile.ProfileFactory>> optionalProfileFactoryClass = profileDefinition.getProfileFactoryClass();
+
+            if ( optionalProfileFactoryClass.isPresent() )
+            {
+                final Profile.ProfileFactory profileFactory;
+                try
+                {
+                    profileFactory = optionalProfileFactoryClass.get().getDeclaredConstructor().newInstance();
+                    return profileFactory.makeFromStoredConfiguration( storedConfiguration, domainID, profileID );
+                }
+                catch ( final InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e )
+                {
+                    throw new IllegalStateException( "unable to create profile instance for " + profileDefinition );
+                }
+            }
+
+            throw new IllegalStateException( "unable to create profile instance for " + profileDefinition + " ( profile factory class not defined )" );
+        }
+
+        public List<String> profileIDsForCategory( final PwmSettingCategory pwmSettingCategory )
+        {
+            final PwmSetting profileSetting = pwmSettingCategory.getProfileSetting().orElseThrow( IllegalStateException::new );
+            return StoredSettingReader.this.readSettingAsStringArray( profileSetting );
+        }
+    }
+
+    private StoredValue readSetting( final PwmSetting setting )
+    {
+        if ( DomainID.systemId().equals( domainID ) )
+        {
+            if ( setting.getCategory().getScope() == PwmSettingScope.DOMAIN )
+            {
+                final String msg = "attempt to read domain scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' as system scope";
+                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
+                throw new IllegalStateException( msg, pwmUnrecoverableException );
+            }
+        }
+        else
+        {
+            if ( setting.getCategory().getScope() == PwmSettingScope.SYSTEM )
+            {
+                final String msg = "attempt to read system scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' as domain scope";
+                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
+                throw new IllegalStateException( msg, pwmUnrecoverableException );
+            }
+        }
+
+        if ( setting.getFlags().contains( PwmSettingFlag.Deprecated ) )
+        {
+            LOGGER.warn( () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
+        }
+
+        if ( StringUtil.isEmpty( profileID ) )
+        {
+            if ( setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalStateException( "attempt to read profiled setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via non-profile" );
+            }
+        }
+        else
+        {
+            if ( !setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalStateException( "attempt to read non-profiled setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via profile" );
+            }
+        }
+
+        final StoredConfigKey key = StoredConfigKey.forSetting( setting, profileID, domainID );
+        return StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
+    }
+
+    public Map<Locale, String> readLocalizedBundle( final PwmLocaleBundle className, final String keyName )
+    {
+        final String key = className + "-" + keyName;
+        if ( dataCache.customText.containsKey( key ) )
+        {
+            return dataCache.customText.get( key );
+        }
+
+        final Map<String, String> storedValue = storedConfiguration.readLocaleBundleMap( className, keyName, domainID );
+        if ( storedValue == null || storedValue.isEmpty() )
+        {
+            dataCache.customText.put( key, null );
+            return null;
+        }
+
+        final Map<Locale, String> localizedMap = new LinkedHashMap<>();
+        for ( final Map.Entry<String, String> entry : storedValue.entrySet() )
+        {
+            final String localeKey = entry.getKey();
+            localizedMap.put( LocaleHelper.parseLocaleString( localeKey ), entry.getValue() );
+        }
+
+        dataCache.customText.put( key, localizedMap );
+        return localizedMap;
+    }
+}

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

@@ -22,7 +22,6 @@ package password.pwm.config.function;
 
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
-import password.pwm.config.DomainConfig;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
@@ -64,8 +63,8 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
             }
             else
             {
-                final DomainConfig domainConfig = new AppConfig( modifier.newStoredConfiguration() ).getDefaultDomainConfig();
-                certs = X509Utils.readRemoteCertificates( URI.create( urlString ), domainConfig );
+                final AppConfig appConfig = new AppConfig( modifier.newStoredConfiguration() );
+                certs = X509Utils.readRemoteCertificates( URI.create( urlString ), appConfig );
             }
         }
         catch ( final Exception e )

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

@@ -58,7 +58,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         if ( ldapUrlsValue != null && ldapUrlsValue.toNativeObject() != null )
         {
             final List<String> ldapUrlStrings = ldapUrlsValue.toNativeObject();
-            resultCertificates.addAll( X509Utils.readCertsForListOfLdapUrls( ldapUrlStrings, pwmRequest.getDomainConfig() ) );
+            resultCertificates.addAll( X509Utils.readCertsForListOfLdapUrls( ldapUrlStrings, pwmRequest.getAppConfig() ) );
         }
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;

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

@@ -30,7 +30,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.i18n.Message;
 import password.pwm.svc.email.EmailServerUtil;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 
 import java.security.cert.X509Certificate;
 import java.util.List;
@@ -50,7 +50,7 @@ public class SmtpCertImportFunction implements SettingUIFunction
         final String profile = key.getProfileID();
 
         final List<X509Certificate> certs = EmailServerUtil.readCertificates( pwmRequest.getAppConfig(), profile );
-        if ( !JavaHelper.isEmpty( certs ) )
+        if ( !CollectionUtil.isEmpty( certs ) )
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
             modifier.writeSetting( key, X509CertificateValue.fromX509( certs ), userIdentity );

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

@@ -38,7 +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.java.CollectionUtil;
 import password.pwm.util.secure.X509Utils;
 
 import java.security.cert.X509Certificate;
@@ -67,7 +67,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
 
         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 ) )
+        if ( !CollectionUtil.isEmpty( syslogConfigStrs ) )
         {
             for ( final String entry : syslogConfigStrs )
             {
@@ -81,8 +81,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
                             final List<X509Certificate> certs = X509Utils.readRemoteCertificates(
                                     syslogConfig.getHost(),
                                     syslogConfig.getPort(),
-                                    new AppConfig( modifier.newStoredConfiguration() ).getDefaultDomainConfig()
-                            );
+                                    new AppConfig( modifier.newStoredConfiguration() ) );
                             if ( certs != null )
                             {
                                 resultCertificates.addAll( certs );

+ 11 - 2
server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -48,12 +48,16 @@ import password.pwm.i18n.Display;
 import password.pwm.ldap.permission.UserPermissionType;
 import password.pwm.ldap.permission.UserPermissionUtility;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -103,12 +107,17 @@ public class UserMatchViewerFunction implements SettingUIFunction
         final PwmApplication tempApplication = PwmApplication.createPwmApplication( pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config ) );
         final StoredValue storedValue = StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
         final List<UserPermission> permissions = ValueTypeConverter.valueToUserPermissions( storedValue );
+        final PwmDomain tempDomain = tempApplication.domains().get( key.getDomainID() );
 
-        validateUserPermissionLdapValues( tempApplication.getDefaultDomain(), permissions );
+        validateUserPermissionLdapValues( tempDomain, permissions );
 
         final int maxSearchSeconds = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_USER_PERMISSION_TIMEOUT_SECONDS ) );
         final TimeDuration maxSearchTime = TimeDuration.of( maxSearchSeconds, TimeDuration.Unit.SECONDS );
-        return UserPermissionUtility.discoverMatchingUsers( tempApplication.getDefaultDomain(), permissions, SessionLabel.SYSTEM_LABEL, maxResultSize, maxSearchTime );
+        final Iterator<UserIdentity> matches =  UserPermissionUtility.discoverMatchingUsers( tempDomain, permissions, SessionLabel.SYSTEM_LABEL, maxResultSize, maxSearchTime );
+        final List<UserIdentity> sortedResults = new ArrayList<>( CollectionUtil.iteratorToList( matches ) );
+        Collections.sort( sortedResults );
+        return Collections.unmodifiableList ( sortedResults );
+
     }
 
     private static void validateUserPermissionLdapValues(

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

@@ -20,7 +20,7 @@
 
 package password.pwm.config.option;
 
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.i18n.Display;
 
 import java.io.Serializable;
@@ -52,13 +52,13 @@ public enum IdentityVerificationMethod implements Serializable, ConfigurationOpt
         return userSelectable;
     }
 
-    public String getLabel( final DomainConfig domainConfig, final Locale locale )
+    public String getLabel( final SettingReader settingReader, final Locale locale )
     {
-        return Display.getLocalizedMessage( locale, this.labelKey, domainConfig );
+        return Display.getLocalizedMessage( locale, this.labelKey, settingReader );
     }
 
-    public String getDescription( final DomainConfig domainConfig, final Locale locale )
+    public String getDescription( final SettingReader settingReader, final Locale locale )
     {
-        return Display.getLocalizedMessage( locale, this.descriptionKey, domainConfig );
+        return Display.getLocalizedMessage( locale, this.descriptionKey, settingReader );
     }
 }

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

@@ -22,7 +22,7 @@ package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.SettingReader;
+import password.pwm.config.StoredSettingReader;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
@@ -43,13 +43,13 @@ public abstract class AbstractProfile implements Profile
 {
     private final String identifier;
     private final StoredConfiguration storedConfiguration;
-    private final SettingReader settingReader;
+    private final StoredSettingReader settingReader;
 
     AbstractProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
     {
         this.identifier = identifier;
         this.storedConfiguration = storedConfiguration;
-        this.settingReader = new SettingReader( storedConfiguration, identifier, domainID );
+        this.settingReader = new StoredSettingReader( storedConfiguration, identifier, domainID );
     }
 
     @Override
@@ -140,7 +140,7 @@ public abstract class AbstractProfile implements Profile
         return storedConfiguration;
     }
 
-    protected SettingReader getSettingReader()
+    protected StoredSettingReader getSettingReader()
     {
         return settingReader;
     }

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

@@ -28,7 +28,7 @@ import com.novell.ldapchai.exception.ChaiValidationException;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.SettingReader;
+import password.pwm.config.StoredSettingReader;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.config.value.data.UserPermission;
@@ -81,7 +81,7 @@ public class ChallengeProfile implements Profile, Serializable
             final StoredConfiguration storedConfiguration
     )
     {
-        final SettingReader settingReader = new SettingReader( storedConfiguration, profileID, domainID );
+        final StoredSettingReader settingReader = new StoredSettingReader( storedConfiguration, profileID, domainID );
 
         final int minRandomRequired = Math.toIntExact( settingReader.readSettingAsLong( PwmSetting.CHALLENGE_MIN_RANDOM_REQUIRED ) );
 
@@ -180,7 +180,7 @@ public class ChallengeProfile implements Profile, Serializable
     }
 
     private static ChallengeSet readChallengeSet(
-            final SettingReader settingReader,
+            final StoredSettingReader settingReader,
             final Locale locale,
             final PwmSetting requiredChallenges,
             final PwmSetting randomChallenges,

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

@@ -204,7 +204,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     {
         final String testUserDN = this.readSettingAsString( pwmSetting );
 
-        if ( !StringUtil.isEmpty( testUserDN ) )
+        if ( StringUtil.notEmpty( testUserDN ) )
         {
             return UserIdentity.create( testUserDN, this.getIdentifier(), pwmDomain.getDomainID() ).canonicalized( pwmDomain.getPwmApplication() );
         }

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

@@ -28,7 +28,6 @@ import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
 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.stored.StoredConfiguration;
@@ -74,7 +73,8 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public PwmPasswordPolicy getNewUserPasswordPolicy( final PwmDomain pwmDomain, final Locale userLocale )
             throws PwmUnrecoverableException
     {
-        final long maxNewUserCacheMS = Long.parseLong( pwmDomain.getConfig().readAppProperty( AppProperty.CONFIG_NEWUSER_PASSWORD_POLICY_CACHE_MS ) );
+        final DomainConfig domainConfig = pwmDomain.getConfig();
+        final long maxNewUserCacheMS = Long.parseLong( domainConfig.getAppConfig().readAppProperty( AppProperty.CONFIG_NEWUSER_PASSWORD_POLICY_CACHE_MS ) );
         if ( newUserPasswordPolicyCacheTime != null && TimeDuration.fromCurrent( newUserPasswordPolicyCacheTime ).isLongerThan( maxNewUserCacheMS ) )
         {
             newUserPasswordPolicyCacheTime = Instant.now();
@@ -88,7 +88,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
         }
 
         final PwmPasswordPolicy thePolicy;
-        final LdapProfile ldapProfile = getLdapProfile();
+        final LdapProfile ldapProfile = getLdapProfile( domainConfig );
         final String configuredNewUserPasswordDN = readSettingAsString( PwmSetting.NEWUSER_PASSWORD_POLICY_USER );
         if ( StringUtil.isEmpty( configuredNewUserPasswordDN ) )
         {
@@ -177,12 +177,11 @@ public class NewUserProfile extends AbstractProfile implements Profile
         }
     }
 
-    public LdapProfile getLdapProfile()
+    public LdapProfile getLdapProfile( final DomainConfig domainConfig )
             throws PwmUnrecoverableException
     {
-        final DomainConfig domainConfig = new AppConfig( getStoredConfiguration() ).getDefaultDomainConfig();
         final String configuredProfile = readSettingAsString( PwmSetting.NEWUSER_LDAP_PROFILE );
-        if ( !StringUtil.isEmpty( configuredProfile ) )
+        if ( StringUtil.notEmpty( configuredProfile ) )
         {
             final LdapProfile ldapProfile = domainConfig.getLdapProfiles().get( configuredProfile );
             if ( ldapProfile == null )

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

@@ -27,14 +27,16 @@ import lombok.Builder;
 import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.SettingReader;
+import password.pwm.config.StoredSettingReader;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.LazySupplier;
@@ -54,6 +56,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
@@ -73,16 +76,18 @@ public class PwmPasswordPolicy implements Profile, Serializable
     private final transient Supplier<List<HealthRecord>> healthChecker = new LazySupplier<>( () -> doHealthChecks( this ) );
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
 
+    private final DomainID domainID;
     private final Map<String, String> policyMap;
     private final PolicyMetaData policyMetaData;
 
     private PwmPasswordPolicy(
+            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
     )
     {
-        final Map<String, String> effectivePolicyMap = new HashMap<>();
+        final Map<String, String> effectivePolicyMap = new TreeMap<>();
         if ( policyMap != null )
         {
             effectivePolicyMap.putAll( policyMap );
@@ -99,31 +104,35 @@ public class PwmPasswordPolicy implements Profile, Serializable
             }
         }
 
+        this.domainID = domainID;
         this.chaiPasswordPolicy = chaiPasswordPolicy;
         this.policyMetaData = policyMetaData == null ? PolicyMetaData.builder().build() : policyMetaData;
-        this.policyMap = Collections.unmodifiableMap( effectivePolicyMap );
+        this.policyMap = Map.copyOf( effectivePolicyMap );
     }
 
-    public static PwmPasswordPolicy createPwmPasswordPolicy( final Map<String, String> policyMap )
+    public static PwmPasswordPolicy createPwmPasswordPolicy(
+            final DomainID domainID, final Map<String, String> policyMap )
     {
-        return createPwmPasswordPolicy( policyMap, null );
+        return createPwmPasswordPolicy( domainID, policyMap, null );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
+            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy
     )
     {
-        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, null );
+        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, null );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
+            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
     )
     {
-        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, policyMetaData );
+        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, policyMetaData );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
@@ -131,7 +140,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
             final String profileID
     )
     {
-        final SettingReader settingReader = new SettingReader( domainConfig.getStoredConfiguration(), profileID,  domainConfig.getDomainID() );
+        final StoredSettingReader settingReader = new StoredSettingReader( domainConfig.getStoredConfiguration(), profileID,  domainConfig.getDomainID() );
         final Map<String, String> passwordPolicySettings = new LinkedHashMap<>();
         for ( final PwmPasswordRule rule : PwmPasswordRule.values() )
         {
@@ -153,7 +162,10 @@ public class PwmPasswordPolicy implements Profile, Serializable
                                 settingReader.readSettingAsStringArray( pwmSetting ), ";;;" );
                         break;
                     case ChangeMessage:
-                        value = settingReader.readSettingAsLocalizedString( pwmSetting, PwmConstants.DEFAULT_LOCALE );
+                        {
+                            final String settingValue = settingReader.readSettingAsLocalizedString( pwmSetting, PwmConstants.DEFAULT_LOCALE );
+                            value = settingValue == null ? "" : settingValue;
+                        }
                         break;
                     case ADComplexityLevel:
                         value = settingReader.readSettingAsEnum( pwmSetting, ADPolicyComplexity.class ).toString();
@@ -199,14 +211,13 @@ public class PwmPasswordPolicy implements Profile, Serializable
                 .changePasswordText( readLocalizedSetting( PwmSetting.PASSWORD_POLICY_CHANGE_MESSAGE, domainConfig, settingReader ) )
                 .build();
 
-        return PwmPasswordPolicy.createPwmPasswordPolicy( passwordPolicySettings, null, policyMetaData );
-
+        return PwmPasswordPolicy.createPwmPasswordPolicy( domainConfig.getDomainID(), passwordPolicySettings, null, policyMetaData );
     }
 
     private static Map<Locale, String> readLocalizedSetting(
             final PwmSetting pwmSetting,
             final DomainConfig domainConfig,
-            final SettingReader settingReader
+            final StoredSettingReader settingReader
     )
     {
         final List<Locale> knownLocales = domainConfig.getAppConfig().getKnownLocales();
@@ -236,6 +247,11 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return getIdentifier();
     }
 
+    public DomainID getDomainID()
+    {
+        return domainID;
+    }
+
     private static PwmPasswordPolicy makeDefaultPolicy()
     {
         PwmPasswordPolicy newDefaultPolicy = null;
@@ -246,7 +262,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
             {
                 defaultPolicyMap.put( rule.getKey(), rule.getDefaultValue() );
             }
-            newDefaultPolicy = createPwmPasswordPolicy( defaultPolicyMap, null );
+            newDefaultPolicy = createPwmPasswordPolicy( DomainID.systemId(), defaultPolicyMap, null );
         }
         catch ( final Throwable t )
         {
@@ -260,8 +276,6 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return DEFAULT_POLICY;
     }
 
-
-
     @Override
     public String toString( )
     {
@@ -283,8 +297,6 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return policyMap.get( rule.getKey() );
     }
 
-
-
     public List<UserPermission> getUserPermissions( )
     {
         return policyMetaData.getUserPermissions();
@@ -292,7 +304,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public Optional<String> getChangeMessage( final Locale locale )
     {
-        if ( JavaHelper.isEmpty( policyMetaData.getChangePasswordText() ) )
+        if ( CollectionUtil.isEmpty( policyMetaData.getChangePasswordText() ) )
         {
             return Optional.ofNullable( getValue( PwmPasswordRule.ChangeMessage ) );
         }
@@ -303,7 +315,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public Optional<String> getRuleText( final Locale locale )
     {
-        if ( JavaHelper.isEmpty( policyMetaData.getRuleText() ) )
+        if ( CollectionUtil.isEmpty( policyMetaData.getRuleText() ) )
         {
             return Optional.empty();
         }
@@ -407,7 +419,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
         final ChaiPasswordPolicy backingPolicy = this.chaiPasswordPolicy != null ? chaiPasswordPolicy : otherPolicy.chaiPasswordPolicy;
         final PolicyMetaData metaData = getPolicyMetaData().merge( otherPolicy.getPolicyMetaData() );
-        return new PwmPasswordPolicy( newPasswordPolicies, backingPolicy, metaData );
+        return new PwmPasswordPolicy( domainID, newPasswordPolicies, backingPolicy, metaData );
     }
 
     private PolicyMetaData getPolicyMetaData()
@@ -506,7 +518,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
                 final String detailMsg = minRule.getLabel( locale, null ) + " (" + minValue + ")"
                         + " > "
                         + maxRule.getLabel( locale, null ) + " (" + maxValue + ")";
-                returnList.add( HealthRecord.forMessage( HealthMessage.Config_PasswordPolicyProblem, policyMetaData.getProfileID(), detailMsg ) );
+                returnList.add( HealthRecord.forMessage(
+                        pwmPasswordPolicy.getDomainID(),
+                        HealthMessage.Config_PasswordPolicyProblem, policyMetaData.getProfileID(), detailMsg ) );
             }
         }
 
@@ -520,7 +534,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
                 final String detailMsg = PwmPasswordRule.CharGroupsValues.getLabel( locale, null ) + " (" + minValue + ")"
                         + " > "
                         + PwmPasswordRule.CharGroupsMinMatch.getLabel( locale, null ) + " (" + maxValue + ")";
-                returnList.add( HealthRecord.forMessage( HealthMessage.Config_PasswordPolicyProblem, policyMetaData.getProfileID(), detailMsg ) );
+                returnList.add( HealthRecord.forMessage(
+                        pwmPasswordPolicy.getDomainID(),
+                        HealthMessage.Config_PasswordPolicyProblem, policyMetaData.getProfileID(), detailMsg ) );
             }
         }
 
@@ -546,9 +562,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
         private PolicyMetaData merge( final PolicyMetaData otherPolicy )
         {
             return PolicyMetaData.builder()
-                    .ruleText( JavaHelper.isEmpty( ruleText ) ? otherPolicy.ruleText : ruleText )
-                    .changePasswordText( JavaHelper.isEmpty( changePasswordText ) ? otherPolicy.changePasswordText : changePasswordText )
-                    .userPermissions( JavaHelper.isEmpty( userPermissions ) ? otherPolicy.userPermissions : userPermissions )
+                    .ruleText( CollectionUtil.isEmpty( ruleText ) ? otherPolicy.ruleText : ruleText )
+                    .changePasswordText( CollectionUtil.isEmpty( changePasswordText ) ? otherPolicy.changePasswordText : changePasswordText )
+                    .userPermissions( CollectionUtil.isEmpty( userPermissions ) ? otherPolicy.userPermissions : userPermissions )
                     .profileID( StringUtil.isEmpty( profileID ) ? otherPolicy.profileID : profileID )
                     .build();
         }

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

@@ -24,7 +24,7 @@ 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.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -71,7 +71,7 @@ public class ConfigSearchMachine
         return StoredConfigurationUtil.allPossibleSettingKeysForConfiguration( storedConfiguration )
                 .parallelStream()
                 .filter( k -> k.getRecordType() == StoredConfigKey.RecordType.SETTING )
-                .filter( k -> JavaHelper.isEmpty( domainScope ) || domainScope.contains( k.getDomainID() ) )
+                .filter( k -> CollectionUtil.isEmpty( domainScope ) || domainScope.contains( k.getDomainID() ) )
                 .filter( k -> matchSetting( k, searchTerm ) )
                 .sorted()
                 .collect( Collectors.toCollection( LinkedHashSet::new ) );

+ 14 - 15
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -31,9 +31,9 @@ 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.CollectionUtil;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -49,12 +49,12 @@ class ConfigurationCleaner
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationCleaner.class );
 
     private static final List<PwmExceptionLoggingConsumer<StoredConfigurationModifier>> STORED_CONFIG_POST_PROCESSORS = List.of(
-                    new UpdateDeprecatedAdComplexitySettings(),
-                    new UpdateDeprecatedMinPwdLifetimeSetting(),
-                    new UpdateDeprecatedPublicHealthSetting(),
-                    new ProfileNonProfiledSettings(),
-                    new CheckForSuperfluousProfileSettings(),
-                    new RemoveDefaultSettings() );
+            new UpdateDeprecatedAdComplexitySettings(),
+            new UpdateDeprecatedMinPwdLifetimeSetting(),
+            new UpdateDeprecatedPublicHealthSetting(),
+            new ProfileNonProfiledSettings(),
+            new RemoveSuperfluousProfileSettings(),
+            new RemoveDefaultSettings() );
 
     static void postProcessStoredConfig(
             final StoredConfigurationModifier storedConfiguration
@@ -71,7 +71,7 @@ class ConfigurationCleaner
         {
             final StoredConfiguration existingConfig = modifier.newStoredConfiguration();
 
-            modifier.newStoredConfiguration().keys()
+            CollectionUtil.iteratorToStream( 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 ) );
@@ -130,7 +130,7 @@ class ConfigurationCleaner
                 {
                     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() ) )
+                    if ( oldValue.isPresent() && !StoredConfigurationUtil.isDefaultValue( oldConfig, key ) )
                     {
                         final boolean enforceEnabled = ValueTypeConverter.valueToBoolean( oldValue.get() );
                         final StoredValue value = enforceEnabled
@@ -192,7 +192,7 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-            inputConfig.keys()
+            CollectionUtil.iteratorToStream( inputConfig.keys() )
                     .filter( ( key ) -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                     .filter( ( key ) -> key.toPwmSetting().getCategory().hasProfiles() )
                     .filter( ( key ) -> StringUtil.isEmpty( key.getProfileID() ) )
@@ -239,14 +239,14 @@ class ConfigurationCleaner
         }
     }
 
-    private static class CheckForSuperfluousProfileSettings implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
+    private static class RemoveSuperfluousProfileSettings implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
         @Override
         public void accept( final StoredConfigurationModifier modifier )
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-            inputConfig.keys()
+            CollectionUtil.iteratorToStream( inputConfig.keys() )
                     .filter( ( key ) -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                     .filter( ( key ) -> key.toPwmSetting().getCategory().hasProfiles() )
                     .filter( ( key ) -> verifyProfileIsValid( key, inputConfig ) )
@@ -282,10 +282,9 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-            final PwmSettingTemplateSet templateSet = inputConfig.getTemplateSet();
-            inputConfig.keys()
+            CollectionUtil.iteratorToStream( inputConfig.keys() )
                     .filter( ( key ) -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
-                    .filter( key -> !valueIsDefault( key, inputConfig, templateSet ) )
+                    .filter( key -> !valueIsDefault( key, inputConfig, inputConfig.getTemplateSet().get( key.getDomainID() ) ) )
                     .forEach( ( key ) -> removeDefaultValue( key, inputConfig, modifier ) );
         }
 

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

@@ -22,9 +22,9 @@ package password.pwm.config.stored;
 
 import password.pwm.AppAttribute;
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
-import password.pwm.PwmDomain;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
@@ -35,8 +35,8 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.FileSystemUtility;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -52,7 +52,7 @@ import java.nio.file.StandardCopyOption;
 import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Read the PWM configuration.
@@ -144,7 +144,7 @@ public class ConfigurationReader
                 final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                         {
                                 errorMsg,
-                                }
+                        }
                 );
                 this.configMode = PwmApplicationMode.ERROR;
                 e.printStackTrace();
@@ -152,13 +152,13 @@ public class ConfigurationReader
             }
 
             final List<String> validationErrorMsgs = StoredConfigurationUtil.validateValues( storedConfiguration );
-            if ( !JavaHelper.isEmpty( validationErrorMsgs ) )
+            if ( !CollectionUtil.isEmpty( validationErrorMsgs ) )
             {
                 final String errorMsg = "value error in config file, please investigate: " + validationErrorMsgs.get( 0 );
                 final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                         {
                                 errorMsg,
-                                }
+                        }
                 );
                 this.configMode = PwmApplicationMode.ERROR;
                 throw new PwmUnrecoverableException( errorInformation );
@@ -170,7 +170,7 @@ public class ConfigurationReader
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                     {
                             errorMsg,
-                            }
+                    }
             );
             this.configMode = PwmApplicationMode.ERROR;
             throw new PwmUnrecoverableException( errorInformation );
@@ -195,20 +195,20 @@ public class ConfigurationReader
 
     public void saveConfiguration(
             final StoredConfiguration storedConfiguration,
-            final PwmDomain pwmDomain,
+            final PwmApplication pwmApplication,
             final SessionLabel sessionLabel
     )
             throws IOException, PwmUnrecoverableException, PwmOperationalException
     {
         File backupDirectory = null;
         int backupRotations = 0;
-        if ( pwmDomain != null )
+        if ( pwmApplication != null )
         {
             final AppConfig domainConfig = new AppConfig( storedConfiguration );
             final String backupDirSetting = domainConfig.readAppProperty( AppProperty.BACKUP_LOCATION );
             if ( backupDirSetting != null && backupDirSetting.length() > 0 )
             {
-                final File pwmPath = pwmDomain.getPwmApplication().getPwmEnvironment().getApplicationPath();
+                final File pwmPath = pwmApplication.getPwmEnvironment().getApplicationPath();
                 backupDirectory = FileSystemUtility.figureFilepath( backupDirSetting, pwmPath );
             }
             backupRotations = Integer.parseInt( domainConfig.readAppProperty( AppProperty.BACKUP_CONFIG_COUNT ) );
@@ -243,14 +243,14 @@ public class ConfigurationReader
             }
         }
 
-        if ( pwmDomain != null && pwmDomain.getAuditManager() != null )
+        if ( pwmApplication != null && pwmApplication.getAuditManager() != null )
         {
-            auditModifiedSettings( pwmDomain, storedConfiguration, sessionLabel );
+            auditModifiedSettings( pwmApplication, storedConfiguration, sessionLabel );
         }
 
         try
         {
-            outputConfigurationFile( storedConfiguration, pwmDomain, sessionLabel, backupRotations, backupDirectory );
+            outputConfigurationFile( storedConfiguration, pwmApplication, sessionLabel, backupRotations, backupDirectory );
         }
         finally
         {
@@ -258,37 +258,49 @@ public class ConfigurationReader
         }
     }
 
-    private static void auditModifiedSettings( final PwmDomain pwmDomain, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
+    private static void auditModifiedSettings( final PwmApplication pwmApplication, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
             throws PwmUnrecoverableException
     {
-        final Set<StoredConfigKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, pwmDomain.getConfig().getStoredConfiguration() );
+        final Instant startTime = Instant.now();
+
+        final StoredConfiguration oldConfig = pwmApplication.getConfig().getStoredConfiguration();
+        final List<StoredConfigKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, oldConfig ).stream()
+                .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) || key.isRecordType( StoredConfigKey.RecordType.LOCALE_BUNDLE ) )
+                .sorted()
+                .collect( Collectors.toUnmodifiableList() );
+
+        int changeCount = 0;
 
         for ( final StoredConfigKey key : changedKeys )
         {
-            if ( key.getRecordType() == StoredConfigKey.RecordType.SETTING
-                    || key.getRecordType() == StoredConfigKey.RecordType.LOCALE_BUNDLE )
-            {
-                final Optional<StoredValue> storedValue = newConfig.readStoredValue( key );
-                if ( storedValue.isPresent() )
-                {
-                    final Optional<ValueMetaData> valueMetaData = newConfig.readMetaData( key );
-                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
-                    final String modifyMessage = "configuration record '" + key.getLabel( PwmConstants.DEFAULT_LOCALE )
-                            + "' has been modified, new value: " + storedValue.get().toDebugString( PwmConstants.DEFAULT_LOCALE );
-                    pwmDomain.getAuditManager().submit( sessionLabel, new AuditRecordFactory( pwmDomain ).createUserAuditRecord(
-                            AuditEvent.MODIFY_CONFIGURATION,
-                            userIdentity,
-                            sessionLabel,
-                            modifyMessage
-                    ) );
-                }
-            }
+            final Optional<ValueMetaData> valueMetaData = newConfig.readMetaData( key );
+            final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+
+            final Optional<StoredValue> storedValue = newConfig.readStoredValue( key );
+            String modifyMessage = "configuration record '" + key.getLabel( PwmConstants.DEFAULT_LOCALE ) + "' has been ";
+
+            modifyMessage += storedValue.map( value -> "modified, new value: " + value.toDebugString( PwmConstants.DEFAULT_LOCALE ) )
+                    .orElse( "removed" );
+
+            final String finalMsg = modifyMessage;
+            LOGGER.trace( () -> "sending audit notice: " + finalMsg );
+
+            pwmApplication.getAuditManager().submit( sessionLabel, new AuditRecordFactory( pwmApplication ).createUserAuditRecord(
+                    AuditEvent.MODIFY_CONFIGURATION,
+                    userIdentity,
+                    sessionLabel,
+                    modifyMessage ) );
+
+            changeCount++;
         }
+
+        final int finalChangeCount = changeCount;
+        LOGGER.debug( () -> "sent " + finalChangeCount + " audit notifications about changed settings", () -> TimeDuration.fromCurrent( startTime ) );
     }
 
     private void outputConfigurationFile(
             final StoredConfiguration storedConfiguration,
-            final PwmDomain pwmDomain,
+            final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
             final int backupRotations,
             final File backupDirectory
@@ -306,10 +318,10 @@ public class ConfigurationReader
         }
 
         LOGGER.info( () -> "saved configuration", () -> TimeDuration.fromCurrent( saveFileStartTime ) );
-        if ( pwmDomain != null )
+        if ( pwmApplication != null )
         {
             final String actualChecksum = StoredConfigurationUtil.valueHash( storedConfiguration );
-            pwmDomain.getPwmApplication().writeAppAttribute( AppAttribute.CONFIG_HASH, actualChecksum );
+            pwmApplication.writeAppAttribute( AppAttribute.CONFIG_HASH, actualChecksum );
         }
 
         LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );

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

@@ -58,13 +58,17 @@ class StoredConfigData
     {
         return input.stream()
                 .filter( ( t ) -> t.getKey() != null && t.getMetaData() != null )
-                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getMetaData ) );
+                .collect( Collectors.toMap(
+                        StoredConfigData.ValueAndMetaCarrier::getKey,
+                        StoredConfigData.ValueAndMetaCarrier::getMetaData ) );
     }
 
     static Map<StoredConfigKey, StoredValue> carrierAsStoredValueMap( final Collection<ValueAndMetaCarrier> input )
     {
         return input.stream()
                 .filter( ( t ) -> t.getKey() != null && t.getValue() != null )
-                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getValue ) );
+                .collect( Collectors.toMap(
+                        StoredConfigData.ValueAndMetaCarrier::getKey,
+                        StoredConfigData.ValueAndMetaCarrier::getValue ) );
     }
 }

+ 12 - 31
server/src/main/java/password/pwm/config/stored/StoredConfigKey.java

@@ -35,7 +35,6 @@ import java.io.Serializable;
 import java.util.Comparator;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.stream.Stream;
 
 public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey>
 {
@@ -106,9 +105,9 @@ public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey
         return new StoredConfigKey( RecordType.SETTING, domainID, pwmSetting.getKey(), profileID );
     }
 
-    static StoredConfigKey forLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
+    static StoredConfigKey forLocaleBundle( final PwmLocaleBundle localeBundle, final String key, final DomainID domainID )
     {
-        return new StoredConfigKey( RecordType.LOCALE_BUNDLE, PwmConstants.DOMAIN_ID_PLACEHOLDER, localeBundle.getKey(), key );
+        return new StoredConfigKey( RecordType.LOCALE_BUNDLE, domainID, localeBundle.getKey(), key );
     }
 
     static StoredConfigKey forConfigurationProperty( final ConfigurationProperty configurationProperty )
@@ -147,7 +146,7 @@ public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey
             case SETTING:
             {
                 final PwmSetting pwmSetting = this.toPwmSetting();
-                final boolean hasProfileID = !StringUtil.isEmpty( profileID );
+                final boolean hasProfileID = StringUtil.notEmpty( profileID );
                 if ( pwmSetting.getCategory().hasProfiles() && !hasProfileID )
                 {
                     throw new IllegalStateException( "profileID is required for setting " + pwmSetting.getKey() );
@@ -187,11 +186,15 @@ public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey
         switch ( recordType )
         {
             case SETTING:
-                final boolean correctProfile = toPwmSetting().getCategory().hasProfiles() && !StringUtil.isEmpty( profileID );
-                return correctProfile
-                        ? prefix + toPwmSetting().toMenuLocationDebug( profileID, locale )
-                        : prefix + toPwmSetting().toMenuLocationDebug( null, locale ) + separator + "[profile: " + profileID + "]";
-
+                if ( toPwmSetting().getCategory().hasProfiles()  )
+                {
+                    return prefix + toPwmSetting().toMenuLocationDebug( profileID, locale );
+                }
+                else if ( StringUtil.notEmpty( profileID ) )
+                {
+                    return prefix + toPwmSetting().toMenuLocationDebug( null, locale ) + separator + "[profile: " + profileID + "]";
+                }
+                return prefix + toPwmSetting().toMenuLocationDebug( null, locale );
             case PROPERTY:
                 return prefix + this.getRecordID();
 
@@ -295,28 +298,6 @@ public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey
         }
     }
 
-    public static Stream<StoredConfigKey> filterBySettingSyntax( final PwmSettingSyntax pwmSettingSyntax, final Stream<StoredConfigKey> input )
-    {
-
-        if ( input == null )
-        {
-            return Stream.empty();
-        }
-
-        return filterByType( RecordType.SETTING, input )
-                .filter( ( k ) -> k.toPwmSetting().getSyntax() == pwmSettingSyntax );
-    }
-
-    public static Stream<StoredConfigKey> filterByType( final RecordType recordType, final Stream<StoredConfigKey> input )
-    {
-        if ( input == null )
-        {
-            return Stream.empty();
-        }
-
-        return input.filter( ( k ) -> k.isRecordType( recordType ) );
-    }
-
     public static Comparator<StoredConfigKey> comparator()
     {
         return COMPARATOR;

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

@@ -35,6 +35,7 @@ import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
 import password.pwm.util.java.StringUtil;
@@ -175,7 +176,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             return valueAndMetaWrapper;
         }
 
-        private List<StoredConfigData.ValueAndMetaCarrier> readSettings( )
+        private List<StoredConfigData.ValueAndMetaCarrier> readSettings()
         {
             final Instant startReadSettings = Instant.now();
             final Function<XmlElement, Stream<StoredConfigData.ValueAndMetaCarrier>> readSettingForXmlElement = xmlElement ->
@@ -184,7 +185,10 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                 return valueAndMetaCarrier.stream();
             };
 
+            final Instant startSettingElementXPath = Instant.now();
             final List<XmlElement> settingElements = xpathForAllSetting();
+            perfLog( "startSettingElementXPath", startSettingElementXPath );
+
             final List<StoredConfigData.ValueAndMetaCarrier> results = settingElements
                     .stream()
                     .flatMap( readSettingForXmlElement )
@@ -204,6 +208,8 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
         Optional<StoredConfigData.ValueAndMetaCarrier> readSetting( final XmlElement settingElement )
         {
+            final Instant startReadSetting = Instant.now();
+
             final Optional<String> settingKey = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
             final Optional<String> profileID = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE );
 
@@ -214,7 +220,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 DomainID domainID = readDomainIdForSetting( settingElement, pwmSetting );
                     final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID.orElse( null ), domainID );
                     final ValueMetaData metaData = readMetaDataFromXmlElement( key, settingElement ).orElse( null );
 
@@ -222,6 +228,8 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                             ? null
                             : ValueFactory.fromXmlValues( pwmSetting, settingElement, pwmSecurityKey );
 
+                    perfLog( "startReadSetting: " + key, startReadSetting );
+
                     return Optional.of( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
                 }
             }
@@ -229,15 +237,38 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             return Optional.empty();
         }
 
-        private static DomainID domainIdForSetting( final XmlElement xmlElement, final PwmSetting pwmSetting )
+        private static DomainID readDomainIdForSetting( final XmlElement xmlElement, final PwmSetting pwmSetting )
         {
+            final Optional<DomainID> specifiedDomain = readDomainIDForNonSystemDomainElement( xmlElement );
+            if ( specifiedDomain.isPresent() )
+            {
+                return specifiedDomain.get();
+            }
+
             if ( pwmSetting.getCategory().getScope() == PwmSettingScope.SYSTEM )
             {
                 return DomainID.systemId();
             }
 
-            final String domainID = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_DOMAIN ).orElse( PwmConstants.DOMAIN_ID_DEFAULT.stringValue() );
-            return DomainID.create( domainID );
+            return DomainID.DOMAIN_ID_DEFAULT;
+        }
+
+        private static Optional<DomainID> readDomainIDForNonSystemDomainElement( final XmlElement xmlElement )
+        {
+            final Optional<String> domainID = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_DOMAIN );
+            if ( domainID.isPresent() )
+            {
+                final String domainIdStr = domainID.get();
+                if ( DomainID.systemId().stringValue().equals( domainIdStr ) )
+                {
+                    return Optional.of( DomainID.systemId() );
+                }
+                else
+                {
+                    return Optional.of( DomainID.create( domainIdStr ) );
+                }
+            }
+            return Optional.empty();
         }
 
         public PwmSecurityKey getKey()
@@ -295,7 +326,8 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                                 }
                                 if ( !bundleMap.isEmpty() )
                                 {
-                                    final StoredConfigKey storedConfigKey = StoredConfigKey.forLocaleBundle( pwmLocaleBundle.get(), key.get() );
+                                    final DomainID domainID = readDomainIDForNonSystemDomainElement( xmlElement ).orElse( DomainID.systemId() );
+                                    final StoredConfigKey storedConfigKey = StoredConfigKey.forLocaleBundle( pwmLocaleBundle.get(), key.get(), domainID );
                                     final StoredValue storedValue = new LocalizedStringValue( bundleMap );
                                     final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigKey, xmlElement ).orElse( null );
                                     return Stream.of( new StoredConfigData.ValueAndMetaCarrier( storedConfigKey, storedValue, metaData ) );
@@ -345,7 +377,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                     {
                         userIdentity = UserIdentity.fromDelimitedKey( modifyUserValue.get() );
                     }
-                    catch ( final DateTimeParseException | PwmUnrecoverableException e )
+                    catch ( final Exception e )
                     {
                         LOGGER.trace( () -> "unable to parse userIdentity attribute metadata for key " + key.toString() );
                     }
@@ -527,7 +559,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
             settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, pwmSetting.getKey() );
 
-            if ( !StringUtil.isEmpty( profileID ) )
+            if ( StringUtil.notEmpty( profileID ) )
             {
                 settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, profileID );
             }
@@ -541,7 +573,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             }
 
             final List<XmlElement> valueElements = new ArrayList<>(  );
-            if ( storedConfiguration != null && ValueFactory.isDefaultValue( storedConfiguration.getTemplateSet(), pwmSetting, storedValue ) )
+            if ( storedConfiguration != null && StoredConfigurationUtil.isDefaultValue( storedConfiguration, key ) )
             {
                 final XmlElement defaultValue = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
                 valueElements.add( defaultValue );
@@ -620,31 +652,36 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
         {
             final XmlFactory xmlFactory = XmlFactory.getFactory();
             final List<XmlElement> returnList = new ArrayList<>();
-            for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
+            for ( final DomainID domainID : StoredConfigurationUtil.domainList( storedConfiguration ) )
             {
-                for ( final String key : pwmLocaleBundle.getDisplayKeys() )
+                for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
                 {
-                    final Map<String, String> localeBundle = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
-                    if ( !JavaHelper.isEmpty( localeBundle ) )
+                    for ( final String key : pwmLocaleBundle.getDisplayKeys() )
                     {
-                        final XmlElement localeBundleElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LOCALE_BUNDLE );
-                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE, pwmLocaleBundle.getKey() );
-                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, key );
-
-                        final Map<String, String> localeBundleMap = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
-                        for ( final Map.Entry<String, String> entry : localeBundleMap.entrySet() )
+                        final Map<String, String> localeBundle = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key, domainID );
+                        if ( !CollectionUtil.isEmpty( localeBundle ) )
                         {
-                            final XmlElement valueElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
-                            if ( !StringUtil.isEmpty( entry.getKey() ) )
+                            final XmlElement localeBundleElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LOCALE_BUNDLE );
+                            localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE, pwmLocaleBundle.getKey() );
+                            localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, key );
+
+                            final Map<String, String> localeBundleMap = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key, domainID );
+                            for ( final Map.Entry<String, String> entry : localeBundleMap.entrySet() )
                             {
-                                valueElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE, entry.getKey() );
+                                final XmlElement valueElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                                if ( StringUtil.notEmpty( entry.getKey() ) )
+                                {
+                                    valueElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE, entry.getKey() );
+                                }
+                                valueElement.addText( entry.getValue() );
+                                localeBundleElement.addContent( valueElement );
                             }
-                            valueElement.addText( entry.getValue() );
-                            localeBundleElement.addContent( valueElement );
-                        }
 
-                        decorateElementWithMetaData( storedConfiguration, StoredConfigKey.forLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
-                        returnList.add( localeBundleElement );
+                            final StoredConfigKey storedConfigKey = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, key, domainID );
+                            decorateElementWithDomain( storedConfiguration, storedConfigKey, localeBundleElement );
+                            decorateElementWithMetaData( storedConfiguration, storedConfigKey, localeBundleElement );
+                            returnList.add( localeBundleElement );
+                        }
                     }
                 }
             }
@@ -790,7 +827,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
                 final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
 
-                final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, PwmConstants.DOMAIN_ID_DEFAULT );
+                final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, DomainID.DOMAIN_ID_DEFAULT );
 
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,
@@ -865,7 +902,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
                 final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
 
-                final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null, PwmConstants.DOMAIN_ID_DEFAULT );
+                final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null, DomainID.DOMAIN_ID_DEFAULT );
 
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,

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

@@ -29,6 +29,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -95,7 +96,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                         final String hash = entry.getKey();
                         final FileValue.FileInformation fileInformation = entry.getValue();
                         final ImmutableByteArray realContent = intermediateRepresentation.getExRefs().get( hash );
-                        final FileValue.FileContent realFileContent = new FileValue.FileContent( realContent );
+                        final FileValue.FileContent realFileContent = FileValue.FileContent.fromBytes( realContent );
                         unstrippedMap.put( fileInformation, realFileContent );
                     }
 
@@ -221,7 +222,8 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
     {
         final Map<String, ImmutableByteArray> exRefs = new LinkedHashMap<>();
         final List<SerializedValue> serializedValues = new ArrayList<>();
-        for ( final StoredConfigKey key : storedConfiguration.keys().collect( Collectors.toList() ) )
+        final List<StoredConfigKey> keys = CollectionUtil.iteratorToStream( storedConfiguration.keys() ).collect( Collectors.toList() );
+        for ( final StoredConfigKey key : keys )
         {
             final Optional<StoredValue> storedValue = storedConfiguration.readStoredValue( key );
             if ( storedValue.isPresent() )
@@ -241,7 +243,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                         final FileValue.FileInformation info = entry.getKey();
                         final FileValue.FileContent content = entry.getValue();
                         final String hash = hash( content );
-                        strippedValues.put( info, new FileValue.FileContent( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) ) );
+                        strippedValues.put( info, FileValue.FileContent.fromBytes( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) ) );
                         exRefs.put( hash, content.getContents() );
                     }
                     value = new FileValue( strippedValues );
@@ -259,7 +261,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
         }
 
         final List<SerializedMetaValue> metaValues = new ArrayList<>();
-        for ( final StoredConfigKey key : storedConfiguration.keys().collect( Collectors.toList() ) )
+        for ( final StoredConfigKey key : keys )
         {
             final Optional<ValueMetaData> valueMetaData = storedConfiguration.readMetaData( key );
             valueMetaData.ifPresent( metaData -> metaValues.add( new SerializedMetaValue( key, metaData ) ) );

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

@@ -27,6 +27,7 @@ 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.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.SecureEngine;
@@ -36,6 +37,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -48,6 +50,8 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
     private static final String SETTINGS_FILENAME = "settings.xml";
     private static final String XREF_SUFFIX = ".xref";
 
+    private static final int MAX_ENTRY_BYTES = 10_000_000;
+
     @Override
     public StoredConfiguration readInput( final InputStream inputStream )
             throws PwmUnrecoverableException, IOException
@@ -63,13 +67,13 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
         {
             if ( SETTINGS_FILENAME.equals( zipEntry.getName() ) )
             {
-                final ImmutableByteArray byteArray = JavaHelper.copyToBytes( zipInputStream );
+                final ImmutableByteArray byteArray = JavaHelper.copyToBytes( zipInputStream, MAX_ENTRY_BYTES );
                 storedConfiguration = new StoredConfigXmlSerializer().readInput( byteArray.newByteArrayInputStream() );
             }
             else if ( zipEntry.getName().endsWith( XREF_SUFFIX ) )
             {
                 final String hash = zipEntry.getName().substring( 0, zipEntry.getName().length() - XREF_SUFFIX.length() );
-                final ImmutableByteArray contents = JavaHelper.copyToBytes( zipInputStream );
+                final ImmutableByteArray contents = JavaHelper.copyToBytes( zipInputStream, MAX_ENTRY_BYTES );
                 exrefMap.put( hash, contents );
             }
             zipInputStream.closeEntry();
@@ -116,10 +120,12 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
             final Map<String, ImmutableByteArray> exRefMap,
             final StoredConfigurationModifier modifier
     )
-            throws PwmUnrecoverableException
+            throws PwmUnrecoverableException, IOException
     {
         final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-        for ( final StoredConfigKey key : inputConfig.keys().collect( Collectors.toList() ) )
+        final List<StoredConfigKey> keys = CollectionUtil.iteratorToStream( inputConfig.keys() ).collect( Collectors.toList() );
+
+        for ( final StoredConfigKey key : keys )
         {
             if (
                     StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
@@ -142,7 +148,7 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
                         final ImmutableByteArray realContents = exRefMap.get( hash );
                         if ( realContents != null )
                         {
-                            final FileValue.FileContent realFileContent = new FileValue.FileContent( realContents );
+                            final FileValue.FileContent realFileContent = FileValue.FileContent.fromBytes( realContents );
                             stripedValues.put( info, realFileContent );
                         }
                     }
@@ -159,11 +165,13 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
     }
 
     private Map<String, ImmutableByteArray> extractExRefs( final StoredConfigurationModifier modifier )
-            throws PwmUnrecoverableException
+            throws PwmUnrecoverableException, IOException
     {
         final Map<String, ImmutableByteArray> returnObj = new HashMap<>();
         final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-        for ( final StoredConfigKey key : inputConfig.keys().collect( Collectors.toList() ) )
+        final List<StoredConfigKey> keys = CollectionUtil.iteratorToStream( inputConfig.keys() ).collect( Collectors.toList() );
+
+        for ( final StoredConfigKey key : keys )
         {
             if (
                     StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
@@ -182,7 +190,7 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
                         final FileValue.FileInformation info = entry.getKey();
                         final FileValue.FileContent content = entry.getValue();
                         final String hash = hash( content );
-                        final FileValue.FileContent fileContentWithHash = new FileValue.FileContent( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) );
+                        final FileValue.FileContent fileContentWithHash = FileValue.FileContent.fromBytes( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) );
                         returnObj.put( hash, content.getContents() );
                         stripedValues.put( info, fileContentWithHash );
                     }

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

@@ -20,6 +20,7 @@
 
 package password.pwm.config.stored;
 
+import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.value.StoredValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -27,9 +28,9 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.time.Instant;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Stream;
 
 public interface StoredConfiguration
 {
@@ -41,13 +42,13 @@ public interface StoredConfiguration
 
     Optional<String> readConfigProperty( ConfigurationProperty propertyName );
 
-    PwmSettingTemplateSet getTemplateSet();
+    Map<DomainID, PwmSettingTemplateSet> getTemplateSet();
 
     Optional<ValueMetaData> readSettingMetadata( StoredConfigKey storedConfigKey );
 
-    Map<String, String> readLocaleBundleMap( PwmLocaleBundle bundleName, String keyName );
+    Map<String, String> readLocaleBundleMap( PwmLocaleBundle bundleName, String keyName, DomainID domainID );
 
-    Stream<StoredConfigKey> keys();
+    Iterator<StoredConfigKey> keys();
 
     Optional<ValueMetaData> readMetaData( StoredConfigKey storedConfigKey );
 

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

@@ -22,6 +22,8 @@ package password.pwm.config.stored;
 
 import lombok.Builder;
 import lombok.Value;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.logging.PwmLogger;
 
@@ -59,11 +61,23 @@ public class StoredConfigurationFactory
     public static StoredConfiguration input( final InputStream inputStream )
             throws PwmUnrecoverableException, IOException
     {
-
-        final StoredConfiguration storedConfiguration = SERIALIZER.readInput( inputStream );
-        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier(  storedConfiguration );
-        ConfigurationCleaner.postProcessStoredConfig( modifier );
-        return modifier.newStoredConfiguration();
+        try
+        {
+            final StoredConfiguration storedConfiguration = SERIALIZER.readInput( inputStream );
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+            ConfigurationCleaner.postProcessStoredConfig( modifier );
+            return modifier.newStoredConfiguration();
+        }
+        catch ( final PwmUnrecoverableException | IOException e )
+        {
+            throw e;
+        }
+        catch ( final Exception e )
+        {
+            final String msg = "error reading configuration: " + e.getMessage();
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, msg );
+            throw new PwmUnrecoverableException( errorInformation, e );
+        }
     }
 
     public static void output(

+ 59 - 33
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -20,13 +20,14 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 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;
@@ -36,10 +37,13 @@ import password.pwm.util.secure.PwmSecurityKey;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Stream;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 /**
  * Immutable in-memory configuration.
@@ -52,7 +56,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
     private final Instant modifyTime;
     private final Map<StoredConfigKey, StoredValue> storedValues;
     private final Map<StoredConfigKey, ValueMetaData> metaValues;
-    private final PwmSettingTemplateSet templateSet;
+    private final Map<DomainID, PwmSettingTemplateSet> templateSet;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
 
@@ -60,9 +64,9 @@ public class StoredConfigurationImpl implements StoredConfiguration
     {
         this.createTime = storedConfigData.getCreateTime();
         this.modifyTime = storedConfigData.getModifyTime();
-        this.metaValues = Map.copyOf( storedConfigData.getMetaDatas() );
-        this.templateSet = readTemplateSet( storedConfigData.getStoredValues() );
-        this.storedValues = Map.copyOf( storedConfigData.getStoredValues() );
+        this.metaValues = Map.copyOf( new TreeMap<>( storedConfigData.getMetaDatas() ) );
+        this.templateSet = TemplateSetReader.readTemplateSet( storedConfigData.getStoredValues() );
+        this.storedValues = Map.copyOf( new TreeMap<>( storedConfigData.getStoredValues() ) );
     }
 
     StoredConfigurationImpl()
@@ -71,7 +75,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
         this.modifyTime = Instant.now();
         this.storedValues = Collections.emptyMap();
         this.metaValues = Collections.emptyMap();
-        this.templateSet = readTemplateSet( Collections.emptyMap() );
+        this.templateSet = TemplateSetReader.readTemplateSet( Collections.emptyMap() );
     }
 
     @Override
@@ -98,9 +102,9 @@ public class StoredConfigurationImpl implements StoredConfiguration
     }
 
     @Override
-    public Map<String, String> readLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
+    public Map<String, String> readLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName, final DomainID domainID )
     {
-        final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
+        final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName, domainID );
         final StoredValue value = storedValues.get( key );
         if ( value != null )
         {
@@ -110,45 +114,67 @@ public class StoredConfigurationImpl implements StoredConfiguration
     }
 
     @Override
-    public PwmSettingTemplateSet getTemplateSet()
+    public Map<DomainID, PwmSettingTemplateSet> getTemplateSet()
     {
         return templateSet;
     }
 
-    private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigKey, StoredValue> valueMap )
+    private static class TemplateSetReader
     {
-        final Set<PwmSettingTemplate> templates = EnumSet.noneOf( PwmSettingTemplate.class );
-        readTemplateValue( valueMap, PwmSetting.TEMPLATE_LDAP ).ifPresent( templates::add );
-        readTemplateValue( valueMap, PwmSetting.TEMPLATE_STORAGE ).ifPresent( templates::add );
-        readTemplateValue( valueMap, PwmSetting.DB_VENDOR_TEMPLATE ).ifPresent( templates::add );
-        return new PwmSettingTemplateSet( templates );
-    }
+        private static Map<DomainID, PwmSettingTemplateSet> readTemplateSet( final Map<StoredConfigKey, StoredValue> valueMap )
+        {
+            final List<String> domainStrList = ValueTypeConverter.valueToStringArray( valueMap.getOrDefault(
+                    StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() ),
+                    PwmSetting.DOMAIN_LIST.getDefaultValue( PwmSettingTemplateSet.getDefault() ) ) );
+
+            final List<DomainID> domainIDList = domainStrList.stream().map( DomainID::create ).collect( Collectors.toList() );
+
+            final Map<DomainID, PwmSettingTemplateSet> templateSets = domainIDList.stream().collect( Collectors.toMap(
+                    domainID -> domainID,
+                    domainID -> readTemplateSet( valueMap, domainID )
+            ) );
+            templateSets.put( DomainID.systemId(), PwmSettingTemplateSet.getDefault() );
+            return Map.copyOf( new TreeMap<>( templateSets ) );
+        }
 
-    private static Optional<PwmSettingTemplate> readTemplateValue( final Map<StoredConfigKey, StoredValue> valueMap, final PwmSetting pwmSetting )
-    {
-        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, PwmConstants.DOMAIN_ID_PLACEHOLDER );
-        final StoredValue storedValue = valueMap.get( key );
+        private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigKey, StoredValue> valueMap, final DomainID domain )
+        {
+            final Set<PwmSettingTemplate> templates = EnumSet.noneOf( PwmSettingTemplate.class );
+            readTemplateValue( valueMap, domain, PwmSetting.TEMPLATE_LDAP ).ifPresent( templates::add );
+            readTemplateValue( valueMap, domain, PwmSetting.TEMPLATE_STORAGE ).ifPresent( templates::add );
+            readTemplateValue( valueMap, domain, PwmSetting.DB_VENDOR_TEMPLATE ).ifPresent( templates::add );
+            return new PwmSettingTemplateSet( templates );
+        }
 
-        if ( storedValue != null )
+        private static Optional<PwmSettingTemplate> readTemplateValue(
+                final Map<StoredConfigKey, StoredValue> valueMap,
+                final DomainID domainID,
+                final PwmSetting pwmSetting )
         {
-            try
-            {
-                final String strValue = ( String ) storedValue.toNativeObject();
-                return Optional.ofNullable( JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue ) );
-            }
-            catch ( final IllegalStateException e )
+            final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, domainID );
+            final StoredValue storedValue = valueMap.get( key );
+
+            if ( storedValue != null )
             {
-                LOGGER.error( () -> "error reading template", e );
+                try
+                {
+                    final String strValue = ( String ) storedValue.toNativeObject();
+                    return Optional.ofNullable( JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue ) );
+                }
+                catch ( final IllegalStateException e )
+                {
+                    LOGGER.error( () -> "error reading template", e );
+                }
             }
-        }
 
-        return Optional.empty();
+            return Optional.empty();
+        }
     }
 
     @Override
-    public Stream<StoredConfigKey> keys()
+    public Iterator<StoredConfigKey> keys()
     {
-        return storedValues.keySet().stream();
+        return storedValues.keySet().iterator();
     }
 
     @Override

+ 17 - 22
server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java

@@ -20,8 +20,8 @@
 
 package password.pwm.config.stored;
 
+import password.pwm.bean.DomainID;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StringValue;
@@ -38,11 +38,13 @@ import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class StoredConfigurationModifier
 {
     private final AtomicReference<StoredConfigData> ref = new AtomicReference<>( );
+    private final AtomicInteger modifications = new AtomicInteger();
 
     private StoredConfigurationModifier( final StoredConfiguration storedConfiguration )
     {
@@ -79,25 +81,11 @@ public class StoredConfigurationModifier
         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() )
-            {
-                throw new IllegalArgumentException( "writing of setting " + setting.getKey() + " requires a non-null profileID" );
-            }
-            if ( !StringUtil.isEmpty( profileID ) && !setting.getCategory().hasProfiles() )
-            {
-                throw new IllegalArgumentException( "cannot specify profile for non-profile setting" );
-            }
-
-            return storedConfigData.toBuilder()
-                    .storedValue( key, value )
-                    .metaData( key, valueMetaData )
-                    .build();
-        } );
+                storedConfigData.toBuilder()
+                        .storedValue( key, value )
+                        .metaData( key, valueMetaData )
+                        .build() );
     }
 
     public void writeConfigProperty(
@@ -128,14 +116,14 @@ public class StoredConfigurationModifier
         } );
     }
 
-    public void resetLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
+    public void resetLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName, final DomainID domainID )
             throws PwmUnrecoverableException
     {
         update( ( storedConfigData ) ->
         {
             final Map<StoredConfigKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
 
-            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName, domainID );
             existingStoredValues.remove( key );
 
             return storedConfigData.toBuilder()
@@ -181,6 +169,7 @@ public class StoredConfigurationModifier
     }
 
     public void writeLocaleBundleMap(
+            final DomainID domainID,
             final PwmLocaleBundle pwmLocaleBundle,
             final String keyName,
             final Map<String, String> localeMap
@@ -189,7 +178,7 @@ public class StoredConfigurationModifier
     {
         update( ( storedConfigData ) ->
         {
-            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName, domainID );
             final StoredValue value = new LocalizedStringValue( localeMap );
 
             return storedConfigData.toBuilder()
@@ -198,6 +187,11 @@ public class StoredConfigurationModifier
         } );
     }
 
+    public int modifications()
+    {
+        return modifications.get();
+    }
+
 
     public void setPassword( final String password )
             throws PwmOperationalException, PwmUnrecoverableException
@@ -240,6 +234,7 @@ public class StoredConfigurationModifier
                     throw new RuntimeException( e );
                 }
             } );
+            modifications.incrementAndGet();
         }
         catch ( final RuntimeException e )
         {

+ 64 - 51
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -40,6 +40,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
 import password.pwm.util.java.StringUtil;
@@ -57,10 +58,13 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -112,7 +116,7 @@ public abstract class StoredConfigurationUtil
         final StoredValue storedValue = StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
         final List<String> settingValues = ValueTypeConverter.valueToStringArray( storedValue );
         return settingValues.stream()
-                .filter( value -> !StringUtil.isEmpty( value ) )
+                .filter( value -> StringUtil.notEmpty( value ) )
                 .collect( Collectors.toUnmodifiableList() );
     }
 
@@ -136,9 +140,8 @@ public abstract class StoredConfigurationUtil
             }
         } );
 
-        storedConfig.keys()
-                .parallel()
-                .filter( ( key ) -> StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() ) )
+        CollectionUtil.iteratorToStream( storedConfig.keys() )
+                .filter( ( key ) -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .forEach( valueModifier );
 
         final Optional<String> pwdHash = storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
@@ -179,8 +182,7 @@ public abstract class StoredConfigurationUtil
         };
 
         final Instant startTime = Instant.now();
-        final List<String> errorStrings = storedConfiguration.keys()
-                .parallel()
+        final List<String> errorStrings = CollectionUtil.iteratorToStream( storedConfiguration.keys() )
                 .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .flatMap( validateSettingFunction )
                 .collect( Collectors.toList() );
@@ -197,7 +199,7 @@ public abstract class StoredConfigurationUtil
             return false;
         }
         final Optional<String> passwordHash = storedConfiguration.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        return passwordHash.isPresent() && BCrypt.testAnswer( password, passwordHash.get(), new AppConfig( storedConfiguration ).getDefaultDomainConfig() );
+        return passwordHash.isPresent() && BCrypt.testAnswer( password, passwordHash.get(), new AppConfig( storedConfiguration ) );
     }
 
     public static boolean hasPassword( final StoredConfiguration storedConfiguration )
@@ -214,7 +216,7 @@ public abstract class StoredConfigurationUtil
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                     {
                             "can not set blank password",
-                            }
+                    }
             ) );
         }
         final String trimmedPassword = password.trim();
@@ -223,7 +225,7 @@ public abstract class StoredConfigurationUtil
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                     {
                             "can not set blank password",
-                            }
+                    }
             ) );
         }
 
@@ -252,16 +254,16 @@ public abstract class StoredConfigurationUtil
 
     public static Map<String, String> makeDebugMap(
             final StoredConfiguration storedConfiguration,
-            final Stream<StoredConfigKey> interestedItems,
+            final List<StoredConfigKey> interestedItems,
             final Locale locale
     )
     {
-        return interestedItems
-                .filter( ( key ) -> !key.isRecordType( StoredConfigKey.RecordType.PROPERTY ) )
-                .collect( Collectors.toUnmodifiableMap(
-                        ( key ) -> key.getLabel( locale ),
-                        ( key ) -> StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key ).toDebugString( locale )
-                ) );
+        return Collections.unmodifiableMap( new TreeMap<>( interestedItems.stream()
+                .filter( key -> !key.isRecordType( StoredConfigKey.RecordType.PROPERTY ) )
+                .collect( Collectors.toMap(
+                        key -> key.getLabel( locale ),
+                        key -> StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key ).toDebugString( locale )
+                ) ) ) );
     }
 
     public static Set<StoredConfigKey> allPossibleSettingKeysForConfiguration(
@@ -272,7 +274,6 @@ public abstract class StoredConfigurationUtil
         allDomainIds.add( DomainID.systemId() );
 
         return allDomainIds.stream()
-                .parallel()
                 .flatMap( domainID -> allPossibleSettingKeysForDomain( storedConfiguration, domainID ) )
                 .collect( Collectors.toUnmodifiableSet() );
     }
@@ -313,23 +314,23 @@ public abstract class StoredConfigurationUtil
     {
         final Instant startTime = Instant.now();
 
-        final Stream<StoredConfigKey> interestedReferences = Stream.concat(
-                originalConfiguration.keys(),
-                modifiedConfiguration.keys() ).distinct();
+        final Predicate<StoredConfigKey> hashTester = key ->
+        {
+            final Optional<String> hash1 = originalConfiguration.readStoredValue( key ).map( StoredValue::valueHash );
+            final Optional<String> hash2 = modifiedConfiguration.readStoredValue( key ).map( StoredValue::valueHash );
+            return !hash1.equals( hash2 );
+        };
 
-        final Set<StoredConfigKey> deltaReferences = interestedReferences
-                .parallel()
-                .filter( reference ->
-                        {
-                            final Optional<String> hash1 = originalConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
-                            final Optional<String> hash2 = modifiedConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
-                            return !hash1.equals( hash2 );
-                        }
-                ).collect( Collectors.toSet() );
+        final Set<StoredConfigKey> deltaReferences = Stream.concat(
+                CollectionUtil.iteratorToStream( originalConfiguration.keys() ),
+                CollectionUtil.iteratorToStream( modifiedConfiguration.keys() ) )
+                .distinct()
+                .filter( hashTester )
+                .collect( Collectors.toUnmodifiableSet() );
 
-        LOGGER.trace( () -> "generated changeLog items via compare", () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.trace( () -> "generated " + deltaReferences.size() + " changeLog items via compare", () -> TimeDuration.fromCurrent( startTime ) );
 
-        return Collections.unmodifiableSet( deltaReferences );
+        return deltaReferences;
     }
 
     public static StoredValue getValueOrDefault(
@@ -348,7 +349,7 @@ public abstract class StoredConfigurationUtil
         {
             case SETTING:
             {
-                final PwmSettingTemplateSet templateSet = storedConfiguration.getTemplateSet();
+                final PwmSettingTemplateSet templateSet = storedConfiguration.getTemplateSet().get( key.getDomainID() );
                 return key.toPwmSetting().getDefaultValue( templateSet );
             }
 
@@ -369,10 +370,10 @@ public abstract class StoredConfigurationUtil
 
     public static List<DomainID> domainList( final StoredConfiguration storedConfiguration )
     {
-        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 domainList.stream().map( DomainID::create ).sorted().collect( Collectors.toUnmodifiableList() );
+        return storedConfiguration.getTemplateSet().keySet().stream()
+                .filter( domain -> !Objects.equals( domain, DomainID.systemId() ) )
+                .sorted()
+                .collect( Collectors.toUnmodifiableList() );
     }
 
     public static StoredConfiguration copyProfileID(
@@ -442,6 +443,7 @@ public abstract class StoredConfigurationUtil
     )
             throws PwmUnrecoverableException
     {
+        final Instant startTime = Instant.now();
         final DomainID sourceID = DomainID.create( source );
         final DomainID destinationID = DomainID.create( destination );
 
@@ -460,21 +462,32 @@ public abstract class StoredConfigurationUtil
         }
 
         final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( oldStoredConfiguration );
-        modifier.newStoredConfiguration().keys()
+        CollectionUtil.iteratorToStream( modifier.newStoredConfiguration().keys() )
                 .filter( key -> key.getDomainID().equals( sourceID ) )
                 .forEach( key ->
+                {
+                    final StoredConfigKey newKey = key.withNewDomain( destinationID );
+                    final StoredValue storedValue = oldStoredConfiguration.readStoredValue( key ).orElseThrow();
+                    try
+                    {
+                        modifier.writeSetting( newKey, storedValue, userIdentity );
+                    }
+                    catch ( final PwmUnrecoverableException e )
+                    {
+                        throw new IllegalStateException( "unexpected error copying domain setting values: " + e.getMessage() );
+                    }
+                } );
+
         {
-            final StoredConfigKey newKey = key.withNewDomain( destinationID );
-            final StoredValue storedValue = oldStoredConfiguration.readStoredValue( key ).orElseThrow();
-            try
-            {
-                modifier.writeSetting( newKey, storedValue, userIdentity );
-            }
-            catch ( final PwmUnrecoverableException e )
-            {
-                throw new IllegalStateException( "unexpected error copying domain setting values: " + e.getMessage() );
-            }
-        } );
+            final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() );
+            final List<String> domainList = new ArrayList<>( ValueTypeConverter.valueToStringArray( StoredConfigurationUtil.getValueOrDefault( oldStoredConfiguration, key ) ) );
+            domainList.add( destination );
+            final StoredValue value = new StringArrayValue( domainList );
+            modifier.writeSetting( key, value, userIdentity );
+        }
+
+        LOGGER.trace( () -> "copied " + modifier.modifications() + " domain settings from '" + source + "' to '" + destination + "' domain",
+                () -> TimeDuration.fromCurrent( startTime ) );
 
         return modifier.newStoredConfiguration();
     }
@@ -484,7 +497,7 @@ public abstract class StoredConfigurationUtil
         final Instant startTime = Instant.now();
         final StringBuilder sb = new StringBuilder();
 
-        storedConfiguration.keys()
+        CollectionUtil.iteratorToStream( storedConfiguration.keys() )
                 .map( storedConfiguration::readStoredValue )
                 .flatMap( Optional::stream )
                 .forEach( v -> sb.append( v.valueHash() ) );
@@ -513,9 +526,9 @@ public abstract class StoredConfigurationUtil
         final Optional<StoredValue> existingValue = storedConfiguration.readStoredValue( key );
         if ( existingValue.isEmpty() )
         {
-            return false;
+            return true;
         }
 
-        return ValueFactory.isDefaultValue( storedConfiguration.getTemplateSet(), key.toPwmSetting(), existingValue.get() );
+        return ValueFactory.isDefaultValue( storedConfiguration.getTemplateSet().get( key.getDomainID( ) ), key.toPwmSetting(), existingValue.get() );
     }
 }

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

@@ -28,6 +28,7 @@ import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -142,7 +143,7 @@ public class ActionValue extends AbstractValue implements StoredValue
                             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
                             {
                                 // add success status if empty list
-                                final List<Integer> successStatus = JavaHelper.isEmpty( webAction.getSuccessStatus() )
+                                final List<Integer> successStatus = CollectionUtil.isEmpty( webAction.getSuccessStatus() )
                                         ? Collections.singletonList( 200 )
                                         : webAction.getSuccessStatus();
 
@@ -193,7 +194,7 @@ public class ActionValue extends AbstractValue implements StoredValue
             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
             {
-                if ( !StringUtil.isEmpty( webAction.getPassword() ) )
+                if ( StringUtil.notEmpty( webAction.getPassword() ) )
                 {
                     try
                     {
@@ -275,7 +276,7 @@ public class ActionValue extends AbstractValue implements StoredValue
             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
             for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
             {
-                final String debugPwdValue = !StringUtil.isEmpty( webAction.getPassword() )
+                final String debugPwdValue = StringUtil.notEmpty( webAction.getPassword() )
                         ? PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT
                         : null;
 
@@ -314,11 +315,11 @@ public class ActionValue extends AbstractValue implements StoredValue
                                 ? ""
                                 : PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT
                 );
-                if ( !JavaHelper.isEmpty( webAction.getSuccessStatus() ) )
+                if ( !CollectionUtil.isEmpty( webAction.getSuccessStatus() ) )
                 {
                     sb.append( "\n    successStatus=" ).append( StringUtil.collectionToString( webAction.getSuccessStatus() ) );
                 }
-                if ( !StringUtil.isEmpty( webAction.getBody() ) )
+                if ( StringUtil.notEmpty( webAction.getBody() ) )
                 {
                     sb.append( "\n    body=" ).append( webAction.getBody() );
                 }
@@ -361,7 +362,7 @@ public class ActionValue extends AbstractValue implements StoredValue
             for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
             {
                 final List webActionsList = (List) actionConfigurationMap.get( "webActions" );
-                if ( !JavaHelper.isEmpty( webAction.getCertificates() ) )
+                if ( !CollectionUtil.isEmpty( webAction.getCertificates() ) )
                 {
                     final Map webActionMap = (Map) webActionsList.get( webActionCounter );
                     final List<Map<String, String>> certificateInfos = new ArrayList<>();

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

@@ -21,6 +21,7 @@
 package password.pwm.config.value;
 
 import lombok.Builder;
+import lombok.EqualsAndHashCode;
 import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
@@ -29,6 +30,7 @@ import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -46,6 +48,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.Supplier;
 
 public class FileValue extends AbstractValue implements StoredValue
 {
@@ -59,51 +62,93 @@ public class FileValue extends AbstractValue implements StoredValue
     @Value
     public static class FileInformation implements Serializable
     {
-        private String filename;
-        private String filetype;
+        private static final long serialVersionUID = 1L;
+
+        private final String filename;
+        private final String filetype;
     }
 
-    @Value
+    @EqualsAndHashCode
     public static class FileContent implements Serializable
     {
-        private ImmutableByteArray contents;
+        private static final long serialVersionUID = 1L;
 
-        static FileContent fromEncodedString( final String input )
+        private final String b64EncodedContents;
+        private final transient Supplier<ImmutableByteArray> byteContents = new LazySupplier<>( this::convertToBytes );
+
+        private FileContent( final String b64EncodedContents )
+        {
+            this.b64EncodedContents = b64EncodedContents;
+        }
+
+        public static FileContent fromEncodedString( final String input )
                 throws IOException
         {
-            final String whitespaceStrippedInput = StringUtil.stripAllWhitespace( input );
-            final byte[] convertedBytes = StringUtil.base64Decode( whitespaceStrippedInput );
-            return new FileContent( ImmutableByteArray.of( convertedBytes ) );
+            return new FileContent( input );
+        }
+
+        public static FileContent fromBytes( final ImmutableByteArray contents )
+                throws PwmUnrecoverableException
+        {
+            final String input = StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
+            final String encodedLineBreaks = StringUtil.insertRepeatedLineBreaks( input, PwmConstants.XML_OUTPUT_LINE_WRAP_LENGTH );
+            return new FileContent( encodedLineBreaks );
         }
 
         String toEncodedString( )
                 throws IOException
         {
-            return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
+            return b64EncodedContents;
         }
 
         String sha512sum( )
                 throws PwmUnrecoverableException
         {
-            return SecureEngine.hash( contents.newByteArrayInputStream(), PwmHashAlgorithm.SHA512 );
+            return SecureEngine.hash( byteContents.get().newByteArrayInputStream(), PwmHashAlgorithm.SHA512 );
         }
 
         public int size( )
         {
-            return contents.size();
+            return byteContents.get().size();
+        }
+
+        public ImmutableByteArray getContents()
+        {
+            return byteContents.get();
         }
+
+        private ImmutableByteArray convertToBytes( )
+        {
+            try
+            {
+                final String whitespaceStripped = StringUtil.stripAllWhitespace( b64EncodedContents );
+                final byte[] output = StringUtil.base64Decode( whitespaceStripped, StringUtil.Base64Options.GZIP );
+                return ImmutableByteArray.of( output );
+            }
+            catch ( final Exception e )
+            {
+                throw new IllegalStateException( e );
+            }
+        }
+    }
+
+    public static FileValue newFileValue( final String filename, final String fileMimeType, final ImmutableByteArray contents )
+            throws PwmUnrecoverableException
+    {
+        final FileInformation fileInformation = new FileValue.FileInformation( filename, fileMimeType );
+        final FileContent fileContent = FileContent.fromBytes( contents );
+        return new FileValue( Collections.singletonMap( fileInformation, fileContent ) );
     }
 
     public FileValue( final Map<FileInformation, FileContent> values )
     {
-        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
+        this.values = values == null ? Collections.emptyMap() : Map.copyOf( values );
     }
 
     public static StoredValueFactory factory( )
     {
         return new StoredValueFactory()
         {
-
             @Override
             public FileValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {

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

@@ -26,7 +26,7 @@ import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
@@ -161,7 +161,7 @@ public class FormValue extends AbstractValue implements StoredValue
     @Override
     public String toDebugString( final Locale locale )
     {
-        if ( !JavaHelper.isEmpty( values ) )
+        if ( !CollectionUtil.isEmpty( values ) )
         {
             final StringBuilder sb = new StringBuilder();
             for ( final FormConfiguration formRow : values )
@@ -179,11 +179,11 @@ public class FormValue extends AbstractValue implements StoredValue
                 sb.append( "\n" );
                 sb.append( " Label:" ).append( JsonUtil.serializeMap( formRow.getLabels() ) ).append( "\n" );
                 sb.append( " Description:" ).append( JsonUtil.serializeMap( formRow.getDescription() ) ).append( "\n" );
-                if ( formRow.getType() == FormConfiguration.Type.select && JavaHelper.isEmpty( formRow.getSelectOptions() ) )
+                if ( formRow.getType() == FormConfiguration.Type.select && CollectionUtil.isEmpty( formRow.getSelectOptions() ) )
                 {
                     sb.append( " Select Options: " ).append( JsonUtil.serializeMap( formRow.getSelectOptions() ) ).append( "\n" );
                 }
-                if ( !StringUtil.isEmpty( formRow.getRegex() ) )
+                if ( StringUtil.notEmpty( formRow.getRegex() ) )
                 {
                     sb.append( " Regex:" ).append( formRow.getRegex() )
                             .append( " Regex Error:" ).append( JsonUtil.serializeMap( formRow.getRegexErrors() ) );

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

@@ -24,7 +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.CollectionUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -147,7 +147,7 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
     @Override
     public String toDebugString( final Locale locale )
     {
-        if ( JavaHelper.isEmpty( values ) )
+        if ( CollectionUtil.isEmpty( values ) )
         {
             return "";
         }

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

@@ -22,7 +22,7 @@ package password.pwm.config.value;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.XmlOutputProcessData;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -103,7 +103,7 @@ public class NumericArrayValue extends AbstractValue implements StoredValue
     @Override
     public String toDebugString( final Locale locale )
     {
-        if ( !JavaHelper.isEmpty( values ) )
+        if ( !CollectionUtil.isEmpty( values ) )
         {
             final StringBuilder sb = new StringBuilder();
             for ( final Iterator valueIterator = values.iterator(); valueIterator.hasNext(); )

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

@@ -24,6 +24,7 @@ import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
@@ -117,8 +118,16 @@ public class PrivateKeyValue extends AbstractValue
 
                         if ( !certificates.isEmpty() && privateKey != null )
                         {
-                            final PrivateKeyCertificate privateKeyCertificate = new PrivateKeyCertificate( certificates, privateKey );
-                            return new PrivateKeyValue( privateKeyCertificate );
+                            try
+                            {
+                                final PrivateKeyCertificate privateKeyCertificate = new PrivateKeyCertificate( certificates, privateKey );
+                                return new PrivateKeyValue( privateKeyCertificate );
+                            }
+                            catch ( final PwmUnrecoverableException e )
+                            {
+                                LOGGER.error( () -> "error reading privateKey for setting: '" + pwmSetting.getKey() + "': " + e.getMessage(), e );
+                                e.printStackTrace();
+                            }
                         }
                     }
                 }
@@ -219,7 +228,7 @@ public class PrivateKeyValue extends AbstractValue
                 ? new X509Utils.DebugInfoFlag[]
                 {
                         X509Utils.DebugInfoFlag.IncludeCertificateDetail,
-                        }
+                }
                 : null;
         final Map<String, Object> returnMap = new LinkedHashMap<>();
         returnMap.put( "certificates", X509Utils.makeDebugInfoMap( privateKeyCertificate.getCertificates(), flags ) );

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

@@ -216,7 +216,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         final ArrayList<RemoteWebServiceConfiguration> output = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration remoteWebServiceConfiguration : values )
         {
-            if ( !StringUtil.isEmpty( remoteWebServiceConfiguration.getPassword() ) )
+            if ( StringUtil.notEmpty( remoteWebServiceConfiguration.getPassword() ) )
             {
                 output.add( remoteWebServiceConfiguration.toBuilder().password( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ).build() );
             }

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

@@ -113,7 +113,7 @@ public abstract class StoredValueEncoder
 
         static ParsedInput parseInput( final String value )
         {
-            if ( !StringUtil.isEmpty( value ) )
+            if ( StringUtil.notEmpty( value ) )
             {
                 for ( final Mode mode : Mode.values() )
                 {

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

@@ -21,6 +21,7 @@
 package password.pwm.config.value;
 
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
@@ -75,6 +76,11 @@ public class StringArrayValue extends AbstractValue implements StoredValue
                         .flatMap( Optional::stream )
                         .collect( Collectors.toList() );
 
+                if ( pwmSetting != null && pwmSetting.getFlags().contains( PwmSettingFlag.Sorted ) )
+                {
+                    Collections.sort( values );
+                }
+
                 return new StringArrayValue( values );
             }
         };
@@ -96,7 +102,7 @@ public class StringArrayValue extends AbstractValue implements StoredValue
     @Override
     public List<String> toNativeObject( )
     {
-        return Collections.unmodifiableList( values );
+        return values;
     }
 
     @Override

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

@@ -20,8 +20,11 @@
 
 package password.pwm.config.value;
 
+import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingFlag;
+import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
@@ -89,32 +92,45 @@ public class StringValue extends AbstractValue implements StoredValue
     @Override
     public List<String> validateValue( final PwmSetting pwmSetting )
     {
-        if ( pwmSetting.isRequired() )
+        return validateValue( pwmSetting, value );
+    }
+
+    public static List<String> validateValue( final PwmSetting pwmSetting, final String value )
+    {
+        if ( pwmSetting.isRequired()
+                && StringUtil.isEmpty( value ) )
         {
-            if ( StringUtil.isEmpty( value ) )
-            {
-                return Collections.singletonList( "required value missing" );
-            }
+            return Collections.singletonList( "required value missing" );
         }
 
         final Pattern pattern = pwmSetting.getRegExPattern();
         if ( pattern != null )
         {
             final Matcher matcher = pattern.matcher( value );
-            if ( value != null && value.length() > 0 && !matcher.matches() )
+            if ( StringUtil.notEmpty( value ) && !matcher.matches() )
             {
                 return Collections.singletonList( "incorrect value format for value '" + value + "'" );
             }
         }
 
-        if ( pwmSetting.getFlags().contains( PwmSettingFlag.emailSyntax ) )
+        if ( pwmSetting.getFlags().contains( PwmSettingFlag.emailSyntax )
+                && StringUtil.isEmpty( value )
+                && !FormConfiguration.testEmailAddress( null, value ) )
         {
-            if ( value != null )
+            return Collections.singletonList( "Invalid email address format: '" + value + "'" );
+        }
+
+        if ( StringUtil.notEmpty( value ) && pwmSetting.getSyntax() == PwmSettingSyntax.DOMAIN )
+        {
+            final String lCaseValue = value.toLowerCase( PwmConstants.DEFAULT_LOCALE );
+            final List<String> reservedWords = DomainID.DOMAIN_RESERVED_WORDS;
+            final boolean contains = reservedWords.stream()
+                    .map( String::toLowerCase )
+                    .anyMatch( lCaseValue::contains );
+            if ( contains )
             {
-                if ( !FormConfiguration.testEmailAddress( null, value ) )
-                {
-                    return Collections.singletonList( "Invalid email address format: '" + value + "'" );
-                }
+                return Collections.singletonList( "Domain ID is reserved word: '" + value + "'" );
+
             }
         }
 

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

@@ -85,7 +85,15 @@ public class ValueFactory
         }
 
         final StoredValue defaultValue = pwmSetting.getDefaultValue( templateSet );
-        return Objects.equals( storedValue, defaultValue );
+
+        if ( Objects.equals( defaultValue, storedValue ) )
+        {
+            return true;
+        }
+
+        final String defaultHash = defaultValue.valueHash();
+        final String valueHash = storedValue.valueHash();
+        return Objects.equals( defaultHash, valueHash );
     }
 }
 

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

@@ -32,6 +32,7 @@ import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -335,7 +336,7 @@ public final class ValueTypeConverter
         }
 
         final Set<String> strValues = ( Set<String> ) value.toNativeObject();
-        return JavaHelper.readEnumSetFromStringCollection( enumClass, strValues );
+        return CollectionUtil.readEnumSetFromStringCollection( enumClass, strValues );
     }
 
     public static List<String> valueToProfileID( final PwmSetting profileSetting, final StoredValue storedValue )
@@ -350,7 +351,7 @@ public final class ValueTypeConverter
         final List<String> returnSet = profiles
                 .stream()
                 .distinct()
-                .filter( ( profile ) -> !StringUtil.isEmpty( profile ) )
+                .filter( ( profile ) -> StringUtil.notEmpty( profile ) )
                 .collect( Collectors.toCollection( ArrayList::new ) );
 
         if ( returnSet.isEmpty() )

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

@@ -28,7 +28,7 @@ import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -72,7 +72,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
                 final int minOptionalRequired
         )
         {
-            this.methodSettings = Collections.unmodifiableMap( JavaHelper.copiedEnumMap( methodSettings, IdentityVerificationMethod.class ) );
+            this.methodSettings = Collections.unmodifiableMap( CollectionUtil.copiedEnumMap( methodSettings, IdentityVerificationMethod.class ) );
             this.minOptionalRequired = minOptionalRequired;
         }
 
@@ -105,7 +105,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
 
     private static VerificationMethodSettings normalizeSettings( final VerificationMethodSettings input )
     {
-        final Map<IdentityVerificationMethod, VerificationMethodValue.VerificationMethodSetting> tempMap = JavaHelper.copiedEnumMap(
+        final Map<IdentityVerificationMethod, VerificationMethodValue.VerificationMethodSetting> tempMap = CollectionUtil.copiedEnumMap(
                 input.getMethodSettings(),
                 IdentityVerificationMethod.class );
 

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

@@ -25,6 +25,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
@@ -87,7 +88,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
 
     public boolean hasCertificates( )
     {
-        return !JavaHelper.isEmpty( b64certificates );
+        return !CollectionUtil.isEmpty( b64certificates );
     }
 
     public X509CertificateValue( final List<String> b64certificates )

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

@@ -429,7 +429,7 @@ public class FormConfiguration implements Serializable
                     if ( value.equals( key ) )
                     {
                         final String displayValue = entry.getValue();
-                        if ( !StringUtil.isEmpty( displayValue ) )
+                        if ( StringUtil.notEmpty( displayValue ) )
                         {
                             return displayValue;
                         }

+ 4 - 11
server/src/main/java/password/pwm/error/ErrorInformation.java

@@ -20,8 +20,7 @@
 
 package password.pwm.error;
 
-import password.pwm.PwmDomain;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.http.PwmSession;
 
 import java.io.Serializable;
@@ -120,7 +119,7 @@ public class ErrorInformation implements Serializable
         return sb.toString();
     }
 
-    public String toUserStr( final PwmSession pwmSession, final PwmDomain pwmDomain )
+    public String toUserStr( final PwmSession pwmSession, final SettingReader settingReader )
     {
 
         if ( userStrOverride != null )
@@ -128,23 +127,17 @@ public class ErrorInformation implements Serializable
             return userStrOverride;
         }
 
-        DomainConfig config = null;
         Locale userLocale = null;
 
-        if ( pwmSession != null && pwmDomain.getConfig() != null )
-        {
-            config = pwmDomain.getConfig();
-        }
-
         if ( pwmSession != null )
         {
             userLocale = pwmSession.getSessionStateBean().getLocale();
         }
 
-        return toUserStr( userLocale, config );
+        return toUserStr( userLocale, settingReader );
     }
 
-    public String toUserStr( final Locale userLocale, final DomainConfig config )
+    public String toUserStr( final Locale userLocale, final SettingReader config )
     {
         if ( userStrOverride != null )
         {

+ 2 - 2
server/src/main/java/password/pwm/error/PwmError.java

@@ -21,7 +21,7 @@
 package password.pwm.error;
 
 import com.novell.ldapchai.exception.ChaiError;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 
@@ -378,7 +378,7 @@ public enum PwmError
 
     }
 
-    public String getLocalizedMessage( final Locale locale, final DomainConfig config, final String... fieldValue )
+    public String getLocalizedMessage( final Locale locale, final SettingReader config, final String... fieldValue )
     {
         return LocaleHelper.getLocalizedMessage( locale, this.getResourceKey(), config, password.pwm.i18n.Error.class, fieldValue );
     }

+ 8 - 0
server/src/main/java/password/pwm/error/PwmUnrecoverableException.java

@@ -24,6 +24,8 @@ package password.pwm.error;
 import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 
+import java.io.IOException;
+
 /**
  * A general exception thrown by PWM.
  */
@@ -68,5 +70,11 @@ public class PwmUnrecoverableException extends PwmException
     {
         return new PwmUnrecoverableException( new ErrorInformation( error, message ) );
     }
+
+    public static PwmUnrecoverableException convert( final IOException e )
+    {
+        final String msg = "unexpected io error: " + e.getMessage();
+        return PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
+    }
 }
 

+ 21 - 14
server/src/main/java/password/pwm/health/ApplianceStatusChecker.java

@@ -21,7 +21,7 @@
 package password.pwm.health;
 
 import org.apache.commons.io.FileUtils;
-import password.pwm.PwmDomain;
+import password.pwm.PwmApplication;
 import password.pwm.PwmEnvironment;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
@@ -45,8 +45,9 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 
-public class ApplianceStatusChecker implements HealthChecker
+public class ApplianceStatusChecker implements HealthSupplier
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ApplianceStatusChecker.class );
 
@@ -58,9 +59,15 @@ public class ApplianceStatusChecker implements HealthChecker
     }
 
     @Override
-    public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
     {
-        final boolean isApplianceAvailable = pwmDomain.getPwmApplication().getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance );
+        final Supplier<List<HealthRecord>> supplier = () -> doHealthCheck( pwmApplication );
+        return Collections.singletonList( supplier );
+    }
+
+    public List<HealthRecord> doHealthCheck( final PwmApplication pwmApplication )
+    {
+        final boolean isApplianceAvailable = pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance );
 
         if ( !isApplianceAvailable )
         {
@@ -69,7 +76,7 @@ public class ApplianceStatusChecker implements HealthChecker
 
         try
         {
-            return List.copyOf( readApplianceHealthStatus( pwmDomain ) );
+            return List.copyOf( readApplianceHealthStatus( pwmApplication ) );
         }
         catch ( final Exception e )
         {
@@ -79,18 +86,18 @@ public class ApplianceStatusChecker implements HealthChecker
         return Collections.emptyList();
     }
 
-    private List<HealthRecord> readApplianceHealthStatus( final PwmDomain pwmDomain ) throws IOException, PwmUnrecoverableException, PwmOperationalException
+    private List<HealthRecord> readApplianceHealthStatus( final PwmApplication pwmApplication ) throws IOException, PwmUnrecoverableException, PwmOperationalException
     {
         final List<HealthRecord> healthRecords = new ArrayList<>();
 
-        final String url = figureUrl( pwmDomain );
-        final Map<String, String> requestHeaders = Collections.singletonMap( "sspr-authorization-token", getApplianceAccessToken( pwmDomain ) );
+        final String url = figureUrl( pwmApplication );
+        final Map<String, String> requestHeaders = Collections.singletonMap( "sspr-authorization-token", getApplianceAccessToken( pwmApplication ) );
 
         final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
                 .trustManagerType( PwmHttpClientConfiguration.TrustManagerType.promiscuous )
                 .build();
 
-        final PwmHttpClient pwmHttpClient = pwmDomain.getHttpClientService().getPwmHttpClient( pwmHttpClientConfiguration );
+        final PwmHttpClient pwmHttpClient = pwmApplication.getHttpClientService().getPwmHttpClient( pwmHttpClientConfiguration );
         final PwmHttpClientRequest pwmHttpClientRequest = PwmHttpClientRequest.builder()
                 .method( HttpMethod.GET )
                 .url( url )
@@ -126,9 +133,9 @@ public class ApplianceStatusChecker implements HealthChecker
 
     }
 
-    private String getApplianceAccessToken( final PwmDomain pwmDomain ) throws PwmOperationalException
+    private String getApplianceAccessToken( final PwmApplication pwmApplication ) throws PwmOperationalException
     {
-        final String tokenFile = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceTokenFile );
+        final String tokenFile = pwmApplication.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceTokenFile );
         if ( StringUtil.isEmpty( tokenFile ) )
         {
             final String msg = "unable to determine appliance token, token file environment param "
@@ -143,9 +150,9 @@ public class ApplianceStatusChecker implements HealthChecker
         return "";
     }
 
-    private String figureUrl( final PwmDomain pwmDomain ) throws PwmOperationalException
+    private String figureUrl( final PwmApplication pwmApplication ) throws PwmOperationalException
     {
-        final String hostnameFile = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceHostnameFile );
+        final String hostnameFile = pwmApplication.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceHostnameFile );
         if ( StringUtil.isEmpty( hostnameFile ) )
         {
             final String msg = "unable to determine appliance hostname, hostname file environment param "
@@ -154,7 +161,7 @@ public class ApplianceStatusChecker implements HealthChecker
         }
 
         final String hostname = readFileContents( hostnameFile );
-        final String port = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.AppliancePort );
+        final String port = pwmApplication.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 );

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

@@ -34,6 +34,7 @@ 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.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -47,11 +48,10 @@ import java.util.Collections;
 import java.util.List;
 import java.util.function.Supplier;
 
-public class CertificateChecker implements HealthSupplier, HealthChecker
+public class CertificateChecker implements HealthSupplier
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( CertificateChecker.class );
 
-    @Override
     public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
     {
         final CertificateCheckJob job = new CertificateCheckJob( pwmDomain.getPwmApplication().getConfig() );
@@ -86,7 +86,7 @@ public class CertificateChecker implements HealthSupplier, HealthChecker
                 TimeDuration.Unit.SECONDS );
 
         final List<HealthRecord> records = new ArrayList<>();
-        appConfig.getStoredConfiguration().keys()
+        CollectionUtil.iteratorToStream( appConfig.getStoredConfiguration().keys() )
                 .filter( k -> k.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .forEach( k ->
                 {

+ 96 - 59
server/src/main/java/password/pwm/health/ConfigurationChecker.java

@@ -21,9 +21,13 @@
 package password.pwm.health;
 
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
+import password.pwm.PwmEnvironment;
+import password.pwm.bean.DomainID;
+import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
@@ -43,10 +47,12 @@ import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.servlet.configguide.ConfigGuideForm;
 import password.pwm.i18n.Config;
 import password.pwm.ldap.permission.UserPermissionUtility;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.password.PasswordUtility;
@@ -61,14 +67,15 @@ import java.util.List;
 import java.util.Locale;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-public class ConfigurationChecker implements HealthChecker
+public class ConfigurationChecker implements HealthSupplier
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationChecker.class );
 
     private static final List<Class<? extends ConfigHealthCheck>> ALL_CHECKS = List.of(
+            VerifyNewUserPasswordPolicy.class,
             VerifyBasicConfigs.class,
             VerifyPasswordStrengthLevels.class,
             VerifyPasswordPolicyConfigs.class,
@@ -81,64 +88,51 @@ public class ConfigurationChecker implements HealthChecker
             VerifyUserPermissionSettings.class );
 
     @Override
-    public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
     {
-        if ( pwmDomain.getConfig() == null )
+        final List<Supplier<List<HealthRecord>>> suppliers = new ArrayList<>();
+        suppliers.add( () -> checkAppMode( pwmApplication ) );
+        for ( final PwmDomain domain : pwmApplication.domains().values() )
         {
-            return Collections.emptyList();
+            final Supplier<List<HealthRecord>> supplier = () -> allChecks( domain.getConfig(), PwmConstants.DEFAULT_LOCALE );
+            suppliers.add( supplier );
         }
+        return suppliers;
+    }
 
-        final DomainConfig config = pwmDomain.getConfig();
+    public List<HealthRecord> doHealthCheck( final AppConfig appConfig, final Locale locale )
+    {
+        final List<HealthRecord> healthRecords = new ArrayList<>();
+        for ( final DomainConfig domain : appConfig.getDomainConfigs().values() )
+        {
+           healthRecords.addAll( allChecks( domain, locale ) );
+        }
+        return Collections.unmodifiableList( healthRecords );
+    }
 
+    private List<HealthRecord> checkAppMode( final PwmApplication pwmApplication )
+    {
         final List<HealthRecord> records = new ArrayList<>();
 
-        if ( pwmDomain.getApplicationMode() == PwmApplicationMode.CONFIGURATION )
+        if ( pwmApplication.getApplicationMode() == PwmApplicationMode.CONFIGURATION )
         {
             records.add( HealthRecord.forMessage(
-                    config.getDomainID(),
+                    DomainID.systemId(),
                     HealthMessage.Config_ConfigMode ) );
         }
-
-        if ( config.readSettingAsBoolean( PwmSetting.NEWUSER_ENABLE ) )
-        {
-            for ( final NewUserProfile newUserProfile : config.getNewUserProfiles().values() )
-            {
-                try
-                {
-                    newUserProfile.getNewUserPasswordPolicy( pwmDomain, PwmConstants.DEFAULT_LOCALE );
-                }
-                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() ) );
-                }
-            }
-        }
-
-        records.addAll( doHealthCheck( config, PwmConstants.DEFAULT_LOCALE ) );
-
         return Collections.unmodifiableList( records );
     }
 
-    public List<HealthRecord> doHealthCheck( final DomainConfig config, final Locale locale )
+    private List<HealthRecord> allChecks(
+            final DomainConfig config,
+            final Locale locale
+    )
     {
         if ( config.readSettingAsBoolean( PwmSetting.HIDE_CONFIGURATION_HEALTH_WARNINGS ) )
         {
             return Collections.emptyList();
         }
 
-        return Collections.unmodifiableList( allChecks( config, locale ) );
-    }
-
-
-    private List<HealthRecord> allChecks(
-            final DomainConfig config,
-            final Locale locale
-    )
-    {
         final List<HealthRecord> records = new ArrayList<>();
         for ( final Class<? extends ConfigHealthCheck> clazz : ALL_CHECKS )
         {
@@ -153,7 +147,45 @@ public class ConfigurationChecker implements HealthChecker
                 LOGGER.error( () -> "unexpected error during health check operation for class " + clazz.toString() + ", error:" + e.getMessage(), e );
             }
         }
-        return records;
+        return Collections.unmodifiableList( records );
+    }
+
+    static class VerifyNewUserPasswordPolicy implements ConfigHealthCheck
+    {
+        @Override
+        public List<HealthRecord> healthCheck( final DomainConfig domainConfig, final Locale locale )
+        {
+            final List<HealthRecord> records = new ArrayList<>();
+
+            if ( domainConfig.readSettingAsBoolean( PwmSetting.NEWUSER_ENABLE ) )
+            {
+
+
+
+                for ( final NewUserProfile newUserProfile : domainConfig.getNewUserProfiles().values() )
+                {
+                    try
+                    {
+                        final PwmApplication tempApplication = PwmApplication.createPwmApplication( PwmEnvironment.builder().build()
+                                .makeRuntimeInstance( domainConfig.getAppConfig() ) );
+                        final PwmDomain tempDomain = tempApplication.domains().get( ConfigGuideForm.DOMAIN_ID );
+
+                        newUserProfile.getNewUserPasswordPolicy( tempDomain, PwmConstants.DEFAULT_LOCALE );
+                    }
+                    catch ( final PwmUnrecoverableException e )
+                    {
+                        records.add( HealthRecord.forMessage(
+                                domainConfig.getDomainID(),
+                                HealthMessage.NewUser_PwTemplateBad,
+                                PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
+                                e.getMessage() ) );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( records );
+
+        }
     }
 
 
@@ -255,7 +287,7 @@ public class ConfigurationChecker implements HealthChecker
                 }
             }
 
-            return records;
+            return Collections.unmodifiableList( records );
         }
     }
 
@@ -266,15 +298,14 @@ 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() ) );
+            final List<StoredConfigKey> interestedKeys = CollectionUtil.iteratorToStream( config.getStoredConfiguration().keys() )
+                    .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+                    .filter( key -> key.toPwmSetting().getSyntax() == PwmSettingSyntax.PASSWORD )
+                    .collect( Collectors.toList() );
 
             try
             {
-                for ( final StoredConfigKey key : interestedKeys.collect( Collectors.toList() ) )
+                for ( final StoredConfigKey key : interestedKeys )
                 {
                     final PwmSetting pwmSetting = key.toPwmSetting();
                     final StoredValue storedValue = config.getStoredConfiguration().readStoredValue( key ).orElseThrow();
@@ -283,7 +314,7 @@ public class ConfigurationChecker implements HealthChecker
                     {
                         final String stringValue = passwordValue.getStringValue();
 
-                        if ( !StringUtil.isEmpty( stringValue ) )
+                        if ( StringUtil.notEmpty( stringValue ) )
                         {
                             final int strength = PasswordUtility.judgePasswordStrength( config, stringValue );
                             if ( strength < 50 )
@@ -330,7 +361,9 @@ public class ConfigurationChecker implements HealthChecker
                         final boolean hasResponseAttribute = responseAttr != null && !responseAttr.isEmpty();
                         if ( !hasResponseAttribute )
                         {
-                            records.add( HealthRecord.forMessage( HealthMessage.Config_MissingLDAPResponseAttr,
+                            records.add( HealthRecord.forMessage(
+                                    config.getDomainID(),
+                                    HealthMessage.Config_MissingLDAPResponseAttr,
                                     loopSetting.toMenuLocationDebug( null, locale ),
                                     PwmSetting.CHALLENGE_USER_ATTRIBUTE.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
                             ) );
@@ -432,7 +465,7 @@ public class ConfigurationChecker implements HealthChecker
             for ( final NewUserProfile newUserProfile : config.getNewUserProfiles().values() )
             {
                 final String configuredProfile = newUserProfile.readSettingAsString( PwmSetting.NEWUSER_LDAP_PROFILE );
-                if ( !StringUtil.isEmpty( configuredProfile ) )
+                if ( StringUtil.notEmpty( configuredProfile ) )
                 {
                     final LdapProfile ldapProfile = config.getLdapProfiles().get( configuredProfile );
 
@@ -456,17 +489,19 @@ public class ConfigurationChecker implements HealthChecker
         {
             final List<HealthRecord> records = new ArrayList<>();
 
-            final Stream<StoredConfigKey> interestedKeys = StoredConfigKey.filterBySettingSyntax(
-                    PwmSettingSyntax.FORM, config.getStoredConfiguration().keys() );
+            final List<StoredConfigKey> interestedKeys = CollectionUtil.iteratorToStream( config.getStoredConfiguration().keys() )
+                    .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+                    .filter( key -> key.toPwmSetting().getSyntax() == PwmSettingSyntax.FORM )
+                    .collect( Collectors.toList() );
 
-            for ( final StoredConfigKey key : interestedKeys.collect( Collectors.toList() ) )
+            for ( final StoredConfigKey key : interestedKeys )
             {
                 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 ( !StringUtil.isEmpty( form.getJavascript() ) )
+                    if ( StringUtil.notEmpty( form.getJavascript() ) )
                     {
                         records.add( HealthRecord.forMessage(
                                 config.getDomainID(),
@@ -608,11 +643,13 @@ public class ConfigurationChecker implements HealthChecker
         public List<HealthRecord> healthCheck( final DomainConfig config, final Locale locale )
         {
             final List<HealthRecord> records = new ArrayList<>();
-
             final StoredConfiguration storedConfiguration = config.getStoredConfiguration();
-            for ( final StoredConfigKey configItemKey : StoredConfigKey.filterBySettingSyntax(
-                    PwmSettingSyntax.USER_PERMISSION,
-                    storedConfiguration.keys() ).collect( Collectors.toList() ) )
+            final List<StoredConfigKey> interestedKeys = CollectionUtil.iteratorToStream( config.getStoredConfiguration().keys() )
+                    .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+                    .filter( key -> key.toPwmSetting().getSyntax() == PwmSettingSyntax.USER_PERMISSION )
+                    .collect( Collectors.toList() );
+
+            for ( final StoredConfigKey configItemKey : interestedKeys )
             {
                 final StoredValue storedValue = storedConfiguration.readStoredValue( configItemKey ).orElseThrow( NoSuchElementException::new );
                 final List<UserPermission> permissions = ValueTypeConverter.valueToUserPermissions( storedValue );

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

@@ -21,10 +21,9 @@
 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.config.AppConfig;
 import password.pwm.error.PwmException;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseTable;
@@ -32,25 +31,32 @@ import password.pwm.util.logging.PwmLogger;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 
-public class DatabaseStatusChecker implements HealthChecker
+public class DatabaseStatusChecker implements HealthSupplier
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( DatabaseStatusChecker.class );
 
     @Override
-    public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
     {
-        return Collections.emptyList();
+        final Supplier<List<HealthRecord>> supplier = () -> doHealthCheck( pwmApplication );
+        return Collections.singletonList( supplier );
     }
 
-    public static List<HealthRecord> checkNewDatabaseStatus( final PwmDomain pwmDomain, final DomainConfig config )
+    public List<HealthRecord> doHealthCheck( final PwmApplication pwmApplication )
     {
-        return checkDatabaseStatus( pwmDomain, config );
+        return checkDatabaseStatus( pwmApplication, pwmApplication.getConfig() );
     }
 
-    private static List<HealthRecord> checkDatabaseStatus( final PwmDomain pwmDomain, final DomainConfig config )
+    public static List<HealthRecord> checkNewDatabaseStatus( final PwmApplication pwmApplication, final AppConfig config )
     {
-        if ( !config.getAppConfig().hasDbConfigured() )
+        return checkDatabaseStatus( pwmApplication, config );
+    }
+
+    private static List<HealthRecord> checkDatabaseStatus( final PwmApplication pwmApplication, final AppConfig config )
+    {
+        if ( !config.hasDbConfigured() )
         {
             return Collections.singletonList( HealthRecord.forMessage(
                     DomainID.systemId(),
@@ -61,7 +67,7 @@ public class DatabaseStatusChecker implements HealthChecker
         PwmApplication runtimeInstance = null;
         try
         {
-            final PwmEnvironment runtimeEnvironment = pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() );
+            final PwmEnvironment runtimeEnvironment = pwmApplication.getPwmEnvironment().makeRuntimeInstance( config );
             runtimeInstance = PwmApplication.createPwmApplication( runtimeEnvironment );
             final DatabaseAccessor accessor = runtimeInstance.getDatabaseService().getAccessor();
             accessor.get( DatabaseTable.PWM_META, "test" );

+ 2 - 2
server/src/main/java/password/pwm/health/HealthMessage.java

@@ -20,7 +20,7 @@
 
 package password.pwm.health;
 
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.i18n.Health;
 import password.pwm.util.i18n.LocaleHelper;
 
@@ -141,7 +141,7 @@ public enum HealthMessage
 
     public String getDescription(
             final Locale locale,
-            final DomainConfig config,
+            final SettingReader config,
             final String[] fields )
     {
         return LocaleHelper.getLocalizedMessage( locale, this.getKey(), config, Health.class, fields );

+ 7 - 2
server/src/main/java/password/pwm/health/HealthMonitorSettings.java

@@ -23,7 +23,7 @@ package password.pwm.health;
 import lombok.Builder;
 import lombok.Value;
 import password.pwm.AppProperty;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.AppConfig;
 import password.pwm.util.java.TimeDuration;
 
 import java.io.Serializable;
@@ -32,18 +32,23 @@ import java.io.Serializable;
 @Builder
 class HealthMonitorSettings implements Serializable
 {
+    private boolean healthCheckEnabled;
     private TimeDuration nominalCheckInterval;
     private TimeDuration minimumCheckInterval;
     private TimeDuration maximumRecordAge;
     private TimeDuration maximumForceCheckWait;
+    private TimeDuration threadDumpInterval;
 
-    static HealthMonitorSettings fromConfiguration( final DomainConfig config )
+    static HealthMonitorSettings fromConfiguration( final AppConfig config )
     {
         return HealthMonitorSettings.builder()
+                .healthCheckEnabled( Boolean.parseBoolean( config.readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
                 .nominalCheckInterval( TimeDuration.of( Long.parseLong( config.readAppProperty( AppProperty.HEALTHCHECK_NOMINAL_CHECK_INTERVAL ) ), TimeDuration.Unit.SECONDS ) )
                 .minimumCheckInterval( TimeDuration.of( Long.parseLong( config.readAppProperty( AppProperty.HEALTHCHECK_MIN_CHECK_INTERVAL ) ), TimeDuration.Unit.SECONDS ) )
                 .maximumRecordAge( TimeDuration.of( Long.parseLong( config.readAppProperty( AppProperty.HEALTHCHECK_MAX_RECORD_AGE ) ), TimeDuration.Unit.SECONDS ) )
                 .maximumForceCheckWait( TimeDuration.of( Long.parseLong( config.readAppProperty( AppProperty.HEALTHCHECK_MAX_FORCE_WAIT ) ), TimeDuration.Unit.SECONDS ) )
+                .threadDumpInterval( TimeDuration.of(
+                        Long.parseLong( config.readAppProperty( AppProperty.LOGGING_EXTRA_PERIODIC_THREAD_DUMP_INTERVAL ) ), TimeDuration.Unit.SECONDS ) )
                 .build();
     }
 }

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

@@ -22,9 +22,9 @@ 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.config.SettingReader;
 import password.pwm.ws.server.rest.bean.HealthData;
 
 import java.io.Serializable;
@@ -93,14 +93,6 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         return new HealthRecord( domainID, message.getStatus(), healthTopic, message, fields );
     }
 
-    /**
-     * @deprecated Replace with {@link #forMessage(DomainID, HealthMessage, String...)}
-     */
-    public static HealthRecord forMessage( final HealthMessage message, final String... fields )
-    {
-        return new HealthRecord( PwmConstants.DOMAIN_ID_PLACEHOLDER, message.getStatus(), message.getTopic(), message, fields );
-    }
-
     public HealthStatus getStatus( )
     {
         return status;
@@ -111,7 +103,7 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         return domainID;
     }
 
-    public String getTopic( final Locale locale, final DomainConfig config )
+    public String getTopic( final Locale locale, final SettingReader config )
     {
         if ( topic != null )
         {
@@ -120,7 +112,7 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         return "";
     }
 
-    public String getDetail( final Locale locale, final DomainConfig config )
+    public String getDetail( final Locale locale, final SettingReader config )
     {
         if ( message != null )
         {
@@ -129,7 +121,7 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         return "";
     }
 
-    public String toDebugString( final Locale locale, final DomainConfig config )
+    public String toDebugString( final Locale locale, final SettingReader config )
     {
         return HealthRecord.class.getSimpleName() + " " + status.getDescription( locale, config ) + " " + this.getTopic(
                 locale, config ) + " " + this.getDetail( locale, config );
@@ -156,7 +148,7 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
                 profileRecords, locale, domainConfig );
         return HealthData.builder()
                 .timestamp( Instant.now() )
-                .overall( HealthMonitor.getMostSevereHealthStatus( profileRecords ).toString() )
+                .overall( HealthUtils.getMostSevereHealthStatus( profileRecords ).toString() )
                 .records( healthRecordBeans )
                 .build();
     }

+ 43 - 55
server/src/main/java/password/pwm/health/HealthMonitor.java → server/src/main/java/password/pwm/health/HealthService.java

@@ -23,12 +23,11 @@ package password.pwm.health;
 import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.servlet.configmanager.DebugItemGenerator;
+import password.pwm.util.debug.DebugItemGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
@@ -48,9 +47,7 @@ import java.lang.management.ThreadInfo;
 import java.nio.file.Files;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -61,22 +58,21 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 import java.util.zip.ZipOutputStream;
 
-public class HealthMonitor implements PwmService
+public class HealthService implements PwmService
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( HealthMonitor.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( HealthService.class );
 
-    private static final List<HealthChecker> HEALTH_CHECKERS = List.of(
+    private static final List<HealthSupplier> HEALTH_SUPPLIERS = List.of(
             new LDAPHealthChecker(),
             new JavaChecker(),
-            new CertificateChecker(),
             new ConfigurationChecker(),
             new LocalDBHealthChecker(),
-            new ApplianceStatusChecker() );
+            new ApplianceStatusChecker(),
+            new CertificateChecker() );
 
-    private static final List<HealthSupplier> HEALTH_SUPPLIERS = List.of(
-        new CertificateChecker() );
 
     private ExecutorService executorService;
     private ExecutorService supportZipWriterService;
@@ -86,7 +82,6 @@ public class HealthMonitor implements PwmService
     private final AtomicInteger healthCheckCount = new AtomicInteger( 0 );
 
     private STATUS status = STATUS.CLOSED;
-    private PwmDomain pwmDomain;
     private PwmApplication pwmApplication;
     private volatile HealthData healthData = emptyHealthData();
 
@@ -96,7 +91,7 @@ public class HealthMonitor implements PwmService
         AdPasswordPolicyApiCheck,
     }
 
-    public HealthMonitor( )
+    public HealthService( )
     {
     }
 
@@ -105,11 +100,10 @@ public class HealthMonitor implements PwmService
             throws PwmException
     {
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
-        this.pwmDomain = pwmApplication.getDefaultDomain();
         this.healthData = emptyHealthData();
-        settings = HealthMonitorSettings.fromConfiguration( pwmDomain.getConfig() );
+        settings = HealthMonitorSettings.fromConfiguration( pwmApplication.getConfig() );
 
-        if ( !Boolean.parseBoolean( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
+        if ( !settings.isHealthCheckEnabled() )
         {
             LOGGER.debug( () -> "health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey() );
             status = STATUS.CLOSED;
@@ -120,14 +114,9 @@ public class HealthMonitor implements PwmService
         supportZipWriterService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
         scheduleNextZipOutput();
 
+        if ( settings.getThreadDumpInterval().as( TimeDuration.Unit.SECONDS ) > 0 )
         {
-            final int threadDumpIntervalSeconds = JavaHelper.silentParseInt( pwmDomain.getConfig().readAppProperty(
-                    AppProperty.LOGGING_EXTRA_PERIODIC_THREAD_DUMP_INTERVAL ), 0 );
-            if ( threadDumpIntervalSeconds > 0 )
-            {
-                final TimeDuration interval =  TimeDuration.of( threadDumpIntervalSeconds, TimeDuration.Unit.SECONDS );
-                pwmApplication.getPwmScheduler().scheduleFixedRateJob( new ThreadDumpLogger(), executorService, TimeDuration.SECOND, interval );
-            }
+            pwmApplication.getPwmScheduler().scheduleFixedRateJob( new ThreadDumpLogger(), executorService, TimeDuration.SECOND, settings.getThreadDumpInterval() );
         }
 
         status = STATUS.OPEN;
@@ -149,20 +138,7 @@ public class HealthMonitor implements PwmService
         {
             return HealthStatus.GOOD;
         }
-        return getMostSevereHealthStatus( getHealthRecords( ) );
-    }
-
-    public static HealthStatus getMostSevereHealthStatus( final Collection<HealthRecord> healthRecords )
-    {
-        final EnumSet<HealthStatus> tempSet = EnumSet.noneOf( HealthStatus.class );
-        if ( healthRecords != null )
-        {
-            for ( final HealthRecord record : healthRecords )
-            {
-                tempSet.add( record.getStatus() );
-            }
-        }
-        return HealthStatus.mostSevere( tempSet ).orElse( HealthStatus.GOOD );
+        return HealthUtils.getMostSevereHealthStatus( getHealthRecords( ) );
     }
 
     @Override
@@ -238,11 +214,13 @@ public class HealthMonitor implements PwmService
         final Instant startTime = Instant.now();
         LOGGER.trace( () -> "beginning health check execution #" + counter  );
         final List<HealthRecord> tempResults = new ArrayList<>();
-        for ( final HealthChecker loopChecker : HEALTH_CHECKERS )
+
+
+        for ( final Supplier<List<HealthRecord>> loopSupplier : gatherSuppliers( pwmApplication ) )
         {
             try
             {
-                final List<HealthRecord> loopResults = loopChecker.doHealthCheck( pwmDomain );
+                final List<HealthRecord> loopResults = loopSupplier.get();
                 if ( loopResults != null )
                 {
                     tempResults.addAll( loopResults );
@@ -257,27 +235,37 @@ public class HealthMonitor implements PwmService
             }
         }
 
-        for ( final PwmService service : pwmDomain.getPwmServices() )
+        healthData = new HealthData( Collections.unmodifiableSet( new TreeSet<>( tempResults ) ), Instant.now() );
+        LOGGER.trace( () -> "completed health check execution #" + counter, () -> TimeDuration.fromCurrent( startTime ) );
+    }
+
+    private static List<Supplier<List<HealthRecord>>> gatherSuppliers( final PwmApplication pwmApplication )
+    {
+        final List<Supplier<List<HealthRecord>>> suppliers = new ArrayList<>();
+
+        for ( final PwmService service : pwmApplication.getAppAndDomainPwmServices() )
         {
             try
             {
                 final List<HealthRecord> loopResults = service.healthCheck();
                 if ( loopResults != null )
                 {
-                    tempResults.addAll( loopResults );
+                    final Supplier<List<HealthRecord>> wrappedSupplier = () -> loopResults;
+                    suppliers.add( wrappedSupplier );
                 }
             }
             catch ( final Exception e )
             {
-                if ( status == STATUS.OPEN )
-                {
-                    LOGGER.warn( () -> "unexpected error during healthCheck: " + e.getMessage(), e );
-                }
+                LOGGER.warn( () -> "unexpected error during healthCheck: " + e.getMessage(), e );
             }
         }
 
-        healthData = new HealthData( Collections.unmodifiableSet( new TreeSet<>( tempResults ) ), Instant.now() );
-        LOGGER.trace( () -> "completed health check execution #" + counter, () -> TimeDuration.fromCurrent( startTime ) );
+        for ( final HealthSupplier supplier : HEALTH_SUPPLIERS )
+        {
+            suppliers.addAll( supplier.jobs( pwmApplication ) );
+        }
+
+        return Collections.unmodifiableList( suppliers );
     }
 
     @Override
@@ -340,21 +328,21 @@ public class HealthMonitor implements PwmService
 
     private void scheduleNextZipOutput()
     {
-        final int intervalSeconds = JavaHelper.silentParseInt( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS ), 0 );
+        final int intervalSeconds = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS ), 0 );
         if ( intervalSeconds > 0 )
         {
             final TimeDuration intervalDuration = TimeDuration.of( intervalSeconds, TimeDuration.Unit.SECONDS );
-            pwmApplication.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmDomain ), supportZipWriterService, intervalDuration );
+            pwmApplication.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmApplication ), supportZipWriterService, intervalDuration );
         }
     }
 
     private class SupportZipFileWriter implements Runnable
     {
-        private final PwmDomain pwmDomain;
+        private final PwmApplication pwmApplication;
 
-        SupportZipFileWriter( final PwmDomain pwmDomain )
+        SupportZipFileWriter( final PwmApplication pwmApplication )
         {
-            this.pwmDomain = pwmDomain;
+            this.pwmApplication = pwmApplication;
         }
 
         @Override
@@ -375,14 +363,14 @@ public class HealthMonitor implements PwmService
         private void writeSupportZipToAppPath()
                 throws IOException, PwmUnrecoverableException
         {
-            final File appPath = pwmApplication.getPwmEnvironment().getApplicationPath();
+            final File appPath = HealthService.this.pwmApplication.getPwmEnvironment().getApplicationPath();
             if ( !appPath.exists() )
             {
                 return;
             }
 
-            final int rotationCount = JavaHelper.silentParseInt( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT ), 10 );
-            final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmDomain, SessionLabel.HEALTH_SESSION_LABEL );
+            final int rotationCount = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT ), 10 );
+            final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmApplication, SessionLabel.HEALTH_SESSION_LABEL );
 
             final File supportPath = new File( appPath.getPath() + File.separator + "support" );
 

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

@@ -20,10 +20,10 @@
 
 package password.pwm.health;
 
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.i18n.Health;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.CollectionUtil;
 
 import java.util.Collection;
 import java.util.EnumSet;
@@ -44,7 +44,7 @@ public enum HealthStatus
         return HealthStatus.class.getSimpleName() + "_" + this.toString();
     }
 
-    public String getDescription( final Locale locale, final DomainConfig config )
+    public String getDescription( final Locale locale, final SettingReader config )
     {
         return LocaleHelper.getLocalizedMessage( locale, this.getKey(), config, Health.class );
     }
@@ -52,7 +52,7 @@ public enum HealthStatus
     public static Optional<HealthStatus> mostSevere( final Collection<HealthStatus> healthStatuses )
     {
         // enumset will sort in natural order, with most severe first.
-        final EnumSet<HealthStatus> sortedSet = JavaHelper.copiedEnumSet( healthStatuses, HealthStatus.class );
+        final EnumSet<HealthStatus> sortedSet = CollectionUtil.copiedEnumSet( healthStatuses, HealthStatus.class );
         if ( sortedSet.isEmpty() )
         {
             return Optional.empty();

+ 2 - 2
server/src/main/java/password/pwm/health/HealthTopic.java

@@ -20,7 +20,7 @@
 
 package password.pwm.health;
 
-import password.pwm.config.DomainConfig;
+import password.pwm.config.SettingReader;
 import password.pwm.i18n.Health;
 import password.pwm.util.i18n.LocaleHelper;
 
@@ -46,7 +46,7 @@ public enum HealthTopic
         return HealthTopic.class.getSimpleName() + "_" + this.toString();
     }
 
-    public String getDescription( final Locale locale, final DomainConfig config )
+    public String getDescription( final Locale locale, final SettingReader config )
     {
         return LocaleHelper.getLocalizedMessage( locale, this.getKey(), config, Health.class );
     }

+ 40 - 0
server/src/main/java/password/pwm/health/HealthUtils.java

@@ -0,0 +1,40 @@
+/*
+ * 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 java.util.Collection;
+import java.util.EnumSet;
+
+public class HealthUtils
+{
+    public static HealthStatus getMostSevereHealthStatus( final Collection<HealthRecord> healthRecords )
+    {
+        final EnumSet<HealthStatus> tempSet = EnumSet.noneOf( HealthStatus.class );
+        if ( healthRecords != null )
+        {
+            for ( final HealthRecord record : healthRecords )
+            {
+                tempSet.add( record.getStatus() );
+            }
+        }
+        return HealthStatus.mostSevere( tempSet ).orElse( HealthStatus.GOOD );
+    }
+}

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

@@ -21,26 +21,34 @@
 package password.pwm.health;
 
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
+import password.pwm.PwmApplication;
 import password.pwm.bean.DomainID;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 
-public class JavaChecker implements HealthChecker
+public class JavaChecker implements HealthSupplier
 {
     @Override
-    public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
+    {
+        final Supplier<List<HealthRecord>> supplier = () -> doHealthCheck( pwmApplication );
+        return Collections.singletonList( supplier );
+    }
+
+    public List<HealthRecord> doHealthCheck( final PwmApplication pwmApplication )
     {
         final List<HealthRecord> records = new ArrayList<>();
 
-        final int maxActiveThreads = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MAX_THREADS ) );
+        final int maxActiveThreads = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MAX_THREADS ) );
         if ( Thread.activeCount() > maxActiveThreads )
         {
             records.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Java_HighThreads ) );
         }
 
-        final long minMemory = Long.parseLong( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MIN_HEAP_BYTES ) );
+        final long minMemory = Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MIN_HEAP_BYTES ) );
         if ( Runtime.getRuntime().maxMemory() <= minMemory )
         {
             records.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Java_SmallHeap ) );

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

@@ -56,6 +56,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -84,19 +85,18 @@ import java.util.Set;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
-public class LDAPHealthChecker implements HealthChecker, HealthSupplier
+public class LDAPHealthChecker implements 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()
+        return pwmApplication.domains().values().stream()
                 .map( domain -> ( Supplier<List<HealthRecord>> ) () -> doHealthCheck( domain ) )
                 .collect( Collectors.toList() );
     }
 
-    @Override
     public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
     {
         final DomainConfig config = pwmDomain.getConfig();
@@ -152,7 +152,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
         if ( config.getLdapProfiles() != null && !config.getLdapProfiles().isEmpty() )
         {
             final List<String> urls = config.getLdapProfiles().values().iterator().next().readSettingAsStringArray( PwmSetting.LDAP_SERVER_URLS );
-            if ( urls != null && !urls.isEmpty() && !StringUtil.isEmpty( urls.iterator().next() ) )
+            if ( urls != null && !urls.isEmpty() && StringUtil.notEmpty( urls.iterator().next() ) )
             {
                 returnRecords.addAll( checkVendorSameness( pwmDomain ) );
 
@@ -328,7 +328,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
                         final PasswordStatus passwordStatus;
                         {
                             final UserInfo userInfo = UserInfoFactory.newUserInfo(
-                                    pwmDomain,
+                                    pwmDomain.getPwmApplication(),
                                     SessionLabel.HEALTH_SESSION_LABEL,
                                     locale,
                                     userIdentity,
@@ -391,7 +391,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
             {
                 final UserIdentity userIdentity = UserIdentity.create( theUser.getEntryDN(), ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
                 final UserInfo userInfo = UserInfoFactory.newUserInfo(
-                        pwmDomain,
+                        pwmDomain.getPwmApplication(),
                         SessionLabel.HEALTH_SESSION_LABEL,
                         PwmConstants.DEFAULT_LOCALE,
                         userIdentity,
@@ -713,10 +713,10 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 
     private List<HealthRecord> checkVendorSameness( final PwmDomain pwmDomain )
     {
-        final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
-        if ( healthProperties.containsKey( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck ) )
+        final Map<HealthService.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
+        if ( healthProperties.containsKey( HealthService.HealthMonitorFlag.LdapVendorSameCheck ) )
         {
-            return ( List<HealthRecord> ) healthProperties.get( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck );
+            return ( List<HealthRecord> ) healthProperties.get( HealthService.HealthMonitorFlag.LdapVendorSameCheck );
         }
 
         LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, () -> "beginning check for replica vendor sameness" );
@@ -746,7 +746,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
         }
 
         final ArrayList<HealthRecord> healthRecords = new ArrayList<>();
-        final Set<DirectoryVendor> discoveredVendors = JavaHelper.copiedEnumSet( replicaVendorMap.values(), DirectoryVendor.class );
+        final Set<DirectoryVendor> discoveredVendors = CollectionUtil.copiedEnumSet( replicaVendorMap.values(), DirectoryVendor.class );
 
         if ( discoveredVendors.size() >= 2 )
         {
@@ -763,7 +763,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
             }
             healthRecords.add( HealthRecord.forMessage( pwmDomain.getDomainID(), HealthMessage.LDAP_VendorsNotSame, vendorMsg.toString() ) );
             // cache the error
-            healthProperties.put( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck, healthRecords );
+            healthProperties.put( HealthService.HealthMonitorFlag.LdapVendorSameCheck, healthRecords );
 
             LOGGER.warn( SessionLabel.HEALTH_SESSION_LABEL, () -> "multiple ldap vendors found: " + vendorMsg.toString() );
         }
@@ -772,7 +772,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
             if ( !errorReachingServer )
             {
                 // cache the no errors
-                healthProperties.put( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck, healthRecords );
+                healthProperties.put( HealthService.HealthMonitorFlag.LdapVendorSameCheck, healthRecords );
             }
         }
 
@@ -789,10 +789,10 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 
         if ( pwmDomain.getPwmApplication().getHealthMonitor() != null )
         {
-            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
-            if ( healthProperties.containsKey( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck ) )
+            final Map<HealthService.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
+            if ( healthProperties.containsKey( HealthService.HealthMonitorFlag.AdPasswordPolicyApiCheck ) )
             {
-                final List<HealthRecord> healthRecords = ( List<HealthRecord> ) healthProperties.get( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck );
+                final List<HealthRecord> healthRecords = ( List<HealthRecord> ) healthProperties.get( HealthService.HealthMonitorFlag.AdPasswordPolicyApiCheck );
                 return healthRecords;
             }
         }
@@ -845,8 +845,8 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 
         if ( !errorReachingServer && pwmDomain.getPwmApplication().getHealthMonitor() != null )
         {
-            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
-            healthProperties.put( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck, healthRecords );
+            final Map<HealthService.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
+            healthProperties.put( HealthService.HealthMonitorFlag.AdPasswordPolicyApiCheck, healthRecords );
         }
 
         return healthRecords;
@@ -966,7 +966,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 
             try
             {
-                final LdapProfile ldapProfile = newUserProfile.getLdapProfile();
+                final LdapProfile ldapProfile = newUserProfile.getLdapProfile( pwmDomain.getConfig() );
                 if ( NewUserProfile.TEST_USER_CONFIG_VALUE.equals( policyUserStr ) )
                 {
                     final UserIdentity testUser = ldapProfile.getTestUser( pwmDomain );
@@ -1229,15 +1229,17 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
     {
         final PwmApplication tempApplication = PwmApplication.createPwmApplication(
                 pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() ) );
+        final PwmDomain tempDomain = tempApplication.domains().get( pwmDomain.getDomainID() );
+
         final LDAPHealthChecker ldapHealthChecker = new LDAPHealthChecker();
 
         final LdapProfile ldapProfile = config.getLdapProfiles().get( profileID );
         final List<HealthRecord> profileRecords = new ArrayList<>(
-                ldapHealthChecker.checkBasicLdapConnectivity( tempApplication.getDefaultDomain(), config, ldapProfile, testContextless ) );
+                ldapHealthChecker.checkBasicLdapConnectivity( tempDomain, config, ldapProfile, testContextless ) );
 
         if ( fullTest )
         {
-            profileRecords.addAll( ldapHealthChecker.checkLdapServerUrls( pwmDomain, config, ldapProfile ) );
+            profileRecords.addAll( ldapHealthChecker.checkLdapServerUrls( tempDomain, config, ldapProfile ) );
         }
 
         if ( profileRecords.isEmpty() )
@@ -1247,7 +1249,7 @@ public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 
         if ( fullTest )
         {
-            profileRecords.addAll( ldapHealthChecker.doLdapTestUserCheck( config, ldapProfile, tempApplication.getDefaultDomain() ) );
+            profileRecords.addAll( ldapHealthChecker.doLdapTestUserCheck( config, ldapProfile, tempDomain ) );
         }
 
         return HealthRecord.asHealthDataBean( config, locale, profileRecords );

+ 18 - 12
server/src/main/java/password/pwm/health/LocalDBHealthChecker.java

@@ -21,9 +21,8 @@
 package password.pwm.health;
 
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
+import password.pwm.PwmApplication;
 import password.pwm.bean.DomainID;
-import password.pwm.config.DomainConfig;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
@@ -32,24 +31,32 @@ import password.pwm.util.localdb.LocalDB;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 
-public class LocalDBHealthChecker implements HealthChecker
+public class LocalDBHealthChecker implements HealthSupplier
 {
+
     @Override
-    public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
+    {
+        final Supplier<List<HealthRecord>> supplier = () -> doHealthCheck( pwmApplication );
+        return Collections.singletonList( supplier );
+    }
+
+    public List<HealthRecord> doHealthCheck( final PwmApplication pwmApplication )
     {
-        if ( pwmDomain == null )
+        if ( pwmApplication == null )
         {
             return null;
         }
 
         final List<HealthRecord> healthRecords = new ArrayList<>();
 
-        final LocalDB localDB = pwmDomain.getPwmApplication().getLocalDB();
+        final LocalDB localDB = pwmApplication.getLocalDB();
 
         if ( localDB == null )
         {
-            final String detailedError = pwmDomain.getLastLocalDBFailure() == null ? "unknown, check logs" : pwmDomain.getLastLocalDBFailure().toDebugStr();
+            final String detailedError = pwmApplication.getLastLocalDBFailure() == null ? "unknown, check logs" : pwmApplication.getLastLocalDBFailure().toDebugStr();
             healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_BAD, detailedError ) );
             return healthRecords;
         }
@@ -66,7 +73,7 @@ public class LocalDBHealthChecker implements HealthChecker
             return healthRecords;
         }
 
-        healthRecords.addAll( checkSpaceRemaining( pwmDomain ) );
+        healthRecords.addAll( checkSpaceRemaining( pwmApplication ) );
 
         if ( healthRecords.isEmpty() )
         {
@@ -76,11 +83,10 @@ public class LocalDBHealthChecker implements HealthChecker
         return healthRecords;
     }
 
-    private List<HealthRecord> checkSpaceRemaining( final PwmDomain pwmDomain )
+    private List<HealthRecord> checkSpaceRemaining( final PwmApplication pwmApplication )
     {
-        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.getPwmApplication().getLocalDB().getFileLocation() );
+        final long minFreeSpace = JavaHelper.silentParseLong( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_DISK_MIN_FREE_WARNING ), 500_000_000 );
+        final long freeSpace = FileSystemUtility.diskSpaceRemaining( pwmApplication.getLocalDB().getFileLocation() );
 
         if ( freeSpace < minFreeSpace )
         {

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

@@ -25,7 +25,6 @@ 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;
@@ -36,7 +35,6 @@ 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;
@@ -46,6 +44,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PropertyConfigurationImporter;
 import password.pwm.util.PwmScheduler;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -228,15 +227,15 @@ public class ContextManager implements Serializable
 
         final ParameterReader parameterReader = new ParameterReader( servletContext );
         {
-            final String applicationPathStr = parameterReader.readApplicationPath();
-            if ( applicationPathStr == null || applicationPathStr.isEmpty() )
+            final Optional<String> applicationPathStr = parameterReader.readApplicationPath();
+            if ( applicationPathStr.isEmpty() )
             {
                 startupErrorInformation = new ErrorInformation( PwmError.ERROR_ENVIRONMENT_ERROR, "application path is not specified" );
                 return;
             }
             else
             {
-                applicationPath = new File( applicationPathStr );
+                applicationPath = new File( applicationPathStr.get() );
             }
         }
 
@@ -466,7 +465,7 @@ public class ContextManager implements Serializable
                             storedConfiguration = importer.readConfiguration( fileInputStream );
                         }
 
-                        configReader.saveConfiguration( storedConfiguration, pwmApplication.getDefaultDomain(), SESSION_LABEL );
+                        configReader.saveConfiguration( storedConfiguration, pwmApplication, SESSION_LABEL );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         requestPwmApplicationRestart();
                         success = true;
@@ -591,7 +590,7 @@ public class ContextManager implements Serializable
         return new File( applicationPath.getAbsolutePath() + File.separator + filename );
     }
 
-    public File locateWebInfFilePath( )
+    public Optional<File> locateWebInfFilePath( )
     {
         final String realPath = servletContext.getRealPath( "/WEB-INF" );
 
@@ -600,11 +599,11 @@ public class ContextManager implements Serializable
             final File servletPath = new File( realPath );
             if ( servletPath.exists() )
             {
-                return servletPath;
+                return Optional.of( servletPath );
             }
         }
 
-        return null;
+        return Optional.empty();
     }
 
     private static void outputError( final String outputText )
@@ -629,10 +628,10 @@ public class ContextManager implements Serializable
             this.servletContext = servletContext;
         }
 
-        String readApplicationPath( )
+        Optional<String> readApplicationPath( )
         {
-            final String contextAppPathSetting = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationPath );
-            if ( contextAppPathSetting != null )
+            final Optional<String> contextAppPathSetting = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationPath );
+            if ( contextAppPathSetting.isPresent() )
             {
                 return contextAppPathSetting;
             }
@@ -646,11 +645,10 @@ public class ContextManager implements Serializable
 
         Set<PwmEnvironment.ApplicationFlag> readApplicationFlags( )
         {
-            final String contextAppFlagsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationFlags );
-
-            if ( contextAppFlagsValue != null && !contextAppFlagsValue.isEmpty() )
+            final Optional<String> contextAppFlagsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationFlags );
+            if ( contextAppFlagsValue.isPresent() )
             {
-                return PwmEnvironment.ParseHelper.parseApplicationFlagValueParameter( contextAppFlagsValue );
+                return PwmEnvironment.ParseHelper.parseApplicationFlagValueParameter( contextAppFlagsValue.get() );
             }
 
             final String contextPath = servletContext.getContextPath().replace( "/", "" );
@@ -659,12 +657,12 @@ public class ContextManager implements Serializable
 
         Map<PwmEnvironment.ApplicationParameter, String> readApplicationParams( final File applicationPath  )
         {
-            // attempt to read app params finle from specified env param file value
+            // attempt to read app params file from specified env param file value
             {
-                final String contextAppParamsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationParamFile );
-                if ( !StringUtil.isEmpty( contextAppParamsValue ) )
+                final Optional<String> contextAppParamsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationParamFile );
+                if ( contextAppParamsValue.isPresent() )
                 {
-                    return PwmEnvironment.ParseHelper.readAppParametersFromPath( contextAppParamsValue );
+                    return PwmEnvironment.ParseHelper.readAppParametersFromPath( contextAppParamsValue.get() );
                 }
             }
 
@@ -692,19 +690,17 @@ public class ContextManager implements Serializable
         }
 
 
-        private String readEnvironmentParameter( final PwmEnvironment.EnvironmentParameter environmentParameter )
+        private Optional<String> readEnvironmentParameter( final PwmEnvironment.EnvironmentParameter environmentParameter )
         {
-            final String value = servletContext.getInitParameter(
-                    environmentParameter.toString() );
-
-            if ( value != null && !value.isEmpty() )
+            final String value = servletContext.getInitParameter( environmentParameter.toString() );
+            if ( StringUtil.notEmpty( value ) )
             {
                 if ( !UNSPECIFIED_VALUE.equalsIgnoreCase( value ) )
                 {
-                    return value;
+                    return Optional.of( value );
                 }
             }
-            return null;
+            return Optional.empty();
         }
     }
 
@@ -739,19 +735,19 @@ public class ContextManager implements Serializable
         {
             LOGGER.trace( SESSION_LABEL, () -> "beginning auto-import ldap cert due to config property '"
                     + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'" );
-            final DomainConfig domainConfig = new AppConfig( configReader.getStoredConfiguration() ).getDefaultDomainConfig();
+            final AppConfig appConfig = new AppConfig( configReader.getStoredConfiguration() );
             final StoredConfigurationModifier modifiedConfig = StoredConfigurationModifier.newModifier( configReader.getStoredConfiguration() );
 
             int importedCerts = 0;
-            for ( final DomainID domainID : StoredConfigurationUtil.domainList( configReader.getStoredConfiguration() ) )
+            for ( final DomainConfig domainConfig : appConfig.getDomainConfigs().values() )
             {
                 for ( final LdapProfile ldapProfile : domainConfig.getLdapProfiles().values() )
                 {
                     final List<String> ldapUrls = ldapProfile.getLdapUrls();
-                    if ( !JavaHelper.isEmpty( ldapUrls ) )
+                    if ( !CollectionUtil.isEmpty( ldapUrls ) )
                     {
-                        final Set<X509Certificate> certs = X509Utils.readCertsForListOfLdapUrls( ldapUrls, domainConfig );
-                        if ( !JavaHelper.isEmpty( certs ) )
+                        final Set<X509Certificate> certs = X509Utils.readCertsForListOfLdapUrls( ldapUrls, appConfig );
+                        if ( !CollectionUtil.isEmpty( certs ) )
                         {
                             importedCerts += certs.size();
                             for ( final X509Certificate cert : certs )
@@ -760,7 +756,7 @@ public class ContextManager implements Serializable
                             }
                             final StoredValue storedValue = X509CertificateValue.fromX509( certs );
 
-                            final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), domainID );
+                            final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), domainConfig.getDomainID() );
                             modifiedConfig.writeSetting( key, storedValue, null );
                         }
                     }
@@ -774,7 +770,7 @@ public class ContextManager implements Serializable
                         + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'"
                         + ", imported " + totalImportedCerts + " certificates" );
                 modifiedConfig.writeConfigProperty( ConfigurationProperty.IMPORT_LDAP_CERTIFICATES, "false" );
-                configReader.saveConfiguration( modifiedConfig.newStoredConfiguration(), pwmApplication.getDefaultDomain(), SESSION_LABEL );
+                configReader.saveConfiguration( modifiedConfig.newStoredConfiguration(), pwmApplication, SESSION_LABEL );
                 requestPwmApplicationRestart();
             }
             else

+ 34 - 30
server/src/main/java/password/pwm/http/HttpEventManager.java

@@ -21,8 +21,9 @@
 package password.pwm.http;
 
 import com.novell.ldapchai.util.StringHelper;
-import password.pwm.PwmDomain;
+import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.stats.EpsStatistic;
@@ -64,15 +65,16 @@ public class HttpEventManager implements
         try
         {
             final ContextManager contextManager = ContextManager.getContextManager( httpSession );
-            final PwmDomain pwmDomain = contextManager.getPwmApplication().getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER );
-            httpSession.setAttribute( PwmConstants.SESSION_ATTR_PWM_APP_NONCE, pwmDomain.getPwmApplication().getRuntimeNonce() );
+            final PwmApplication pwmApplication = contextManager.getPwmApplication();
+            httpSession.setAttribute( PwmConstants.SESSION_ATTR_PWM_APP_NONCE, pwmApplication.getRuntimeNonce() );
 
-            if ( pwmDomain.getStatisticsManager() != null )
+            if ( pwmApplication.getStatisticsManager() != null )
             {
-                pwmDomain.getStatisticsManager().updateEps( EpsStatistic.SESSIONS, 1 );
+                pwmApplication.getStatisticsManager().updateEps( EpsStatistic.SESSIONS, 1 );
             }
 
             LOGGER.trace( () -> "new http session created" );
+
         }
         catch ( final PwmUnrecoverableException e )
         {
@@ -84,36 +86,33 @@ public class HttpEventManager implements
     public void sessionDestroyed( final HttpSessionEvent httpSessionEvent )
     {
         final HttpSession httpSession = httpSessionEvent.getSession();
-        try
+        final Map<DomainID, PwmSession> domainIDPwmSessionMap = PwmSessionFactory.getDomainSessionMap( httpSession );
+        for ( final PwmSession pwmSession : domainIDPwmSessionMap.values() )
         {
-            if ( httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION ) != null )
+            try
             {
-                String debugMsg = "destroyed session";
-                final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
-                if ( pwmSession != null )
+                if ( httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION ) != null )
                 {
-                    debugMsg += ": " + makeSessionDestroyedDebugMsg( pwmSession );
+                    final String debugMsg = "destroyed session" + ": " + makeSessionDestroyedDebugMsg( pwmSession );
                     pwmSession.unauthenticateUser( null );
-                }
 
-                final PwmDomain pwmDomain = ContextManager.getPwmApplication( httpSession.getServletContext() )
-                        .getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER );
-                if ( pwmDomain != null )
+                    final PwmApplication pwmApplication = ContextManager.getPwmApplication( httpSession.getServletContext() );
+                    if ( pwmApplication != null )
+                    {
+                        pwmApplication.getSessionTrackService().removeSessionData( pwmSession );
+                    }
+                    LOGGER.trace( pwmSession.getLabel(), () -> debugMsg );
+                }
+                else
                 {
-                    pwmDomain.getSessionTrackService().removeSessionData( pwmSession );
+                    LOGGER.trace( () -> "invalidated uninitialized session" );
                 }
-                final String outputMsg = debugMsg;
-                LOGGER.trace( pwmSession.getLabel(), () -> outputMsg );
             }
-            else
+            catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.trace( () -> "invalidated uninitialized session" );
+                LOGGER.warn( () -> "error during httpSessionDestroyed: " + e.getMessage() );
             }
         }
-        catch ( final PwmUnrecoverableException e )
-        {
-            LOGGER.warn( () -> "error during httpSessionDestroyed: " + e.getMessage() );
-        }
     }
 
 
@@ -165,36 +164,41 @@ public class HttpEventManager implements
     @Override
     public void sessionWillPassivate( final HttpSessionEvent event )
     {
+        /*
         try
         {
-            final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( event.getSession() );
+            final PwmSession pwmSession = PwmSessionFactory.readPwmSession( event.getSession() );
             LOGGER.trace( pwmSession.getLabel(), () -> "passivating session" );
         }
         catch ( final PwmUnrecoverableException e )
         {
             LOGGER.error( () -> "unable to passivate session: " + e.getMessage() );
         }
+
+         */
     }
 
     @Override
     public void sessionDidActivate( final HttpSessionEvent event )
     {
+        /*
         try
         {
             final HttpSession httpSession = event.getSession();
-            final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
+            final PwmSession pwmSession = PwmSessionFactory.readPwmSession( httpSession );
             LOGGER.trace( pwmSession.getLabel(), () -> "activating (de-passivating) session" );
-            final PwmDomain pwmDomain = ContextManager.getPwmApplication( httpSession.getServletContext() )
-                    .getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER );
-            if ( pwmDomain != null )
+            final PwmApplication pwmApplication = ContextManager.getPwmApplication( httpSession.getServletContext() );
+            if ( pwmApplication != null )
             {
-                pwmDomain.getSessionTrackService().addSessionData( pwmSession );
+                pwmApplication.getSessionTrackService().addSessionData( pwmSession );
             }
         }
         catch ( final PwmUnrecoverableException e )
         {
             LOGGER.error( () -> "unable to activate (de-passivate) session: " + e.getMessage() );
         }
+
+         */
     }
 
     private static String makeSessionDestroyedDebugMsg( final PwmSession pwmSession )

+ 9 - 9
server/src/main/java/password/pwm/http/IdleTimeoutCalculator.java

@@ -119,7 +119,7 @@ public class IdleTimeoutCalculator
         if ( domainConfig.readSettingAsBoolean( PwmSetting.HELPDESK_ENABLE ) )
         {
             final String helpdeskProfileID = userInfo.getProfileIDs().get( ProfileDefinition.Helpdesk );
-            if ( !StringUtil.isEmpty( helpdeskProfileID ) )
+            if ( StringUtil.notEmpty( helpdeskProfileID ) )
             {
                 final HelpdeskProfile helpdeskProfile = domainConfig.getHelpdeskProfiles().get( helpdeskProfileID );
                 final long helpdeskIdleTimeout = helpdeskProfile.readSettingAsLong( PwmSetting.HELPDESK_IDLE_TIMEOUT_SECONDS );
@@ -132,7 +132,7 @@ public class IdleTimeoutCalculator
         if ( domainConfig.readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE ) )
         {
             final String peopleSearchID = userInfo.getProfileIDs().get( ProfileDefinition.PeopleSearch );
-            if ( !StringUtil.isEmpty( peopleSearchID ) )
+            if ( StringUtil.notEmpty( peopleSearchID ) )
             {
                 final PeopleSearchProfile peopleSearchProfile = domainConfig.getPeopleSearchProfiles().get( peopleSearchID );
                 final long peopleSearchIdleTimeout = peopleSearchProfile.readSettingAsLong( PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS );
@@ -190,11 +190,11 @@ public class IdleTimeoutCalculator
         }
 
         final DomainConfig config = pwmDomain.getConfig();
-        if ( pwmURL.isPwmServletURL( PwmServletDefinition.Helpdesk ) )
+        if ( pwmURL.matches( PwmServletDefinition.Helpdesk ) )
         {
             if ( config.readSettingAsBoolean( PwmSetting.HELPDESK_ENABLE ) )
             {
-                final HelpdeskProfile helpdeskProfile = pwmSession.getSessionManager().getHelpdeskProfile( );
+                final HelpdeskProfile helpdeskProfile = pwmRequest.getHelpdeskProfile( );
                 if ( helpdeskProfile != null )
                 {
                     final long helpdeskIdleTimeout = helpdeskProfile.readSettingAsLong( PwmSetting.HELPDESK_IDLE_TIMEOUT_SECONDS );
@@ -208,13 +208,13 @@ public class IdleTimeoutCalculator
 
         if (
                 (
-                        pwmURL.isPwmServletURL( PwmServletDefinition.PrivatePeopleSearch )
-                                || pwmURL.isPwmServletURL( PwmServletDefinition.PublicPeopleSearch )
+                        pwmURL.matches( PwmServletDefinition.PrivatePeopleSearch )
+                                || pwmURL.matches( PwmServletDefinition.PublicPeopleSearch )
                 )
                         && pwmURL.isPrivateUrl()
                 )
         {
-            final PeopleSearchProfile peopleSearchProfile = pwmSession.getSessionManager().getPeopleSearchProfile( );
+            final PeopleSearchProfile peopleSearchProfile = pwmRequest.getPeopleSearchProfile( );
             if ( peopleSearchProfile != null )
             {
                 final long peopleSearchIdleTimeout = peopleSearchProfile.readSettingAsLong( PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS );
@@ -225,7 +225,7 @@ public class IdleTimeoutCalculator
             }
         }
 
-        if ( pwmURL.isPwmServletURL( PwmServletDefinition.ConfigEditor ) )
+        if ( pwmURL.matches( PwmServletDefinition.ConfigEditor ) )
         {
             try
             {
@@ -244,7 +244,7 @@ public class IdleTimeoutCalculator
             }
         }
 
-        if ( pwmURL.isPwmServletURL( PwmServletDefinition.ConfigGuide ) )
+        if ( pwmURL.matches( PwmServletDefinition.ConfigGuide ) )
         {
             if ( pwmDomain.getApplicationMode() == PwmApplicationMode.NEW )
             {

+ 55 - 0
server/src/main/java/password/pwm/http/PwmCookiePath.java

@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import password.pwm.PwmConstants;
+import password.pwm.error.PwmUnrecoverableException;
+
+public enum PwmCookiePath
+{
+    Domain,
+    Private,
+    CurrentURL,
+    PwmServlet,;
+
+    String toStringPath( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        switch ( this )
+        {
+            case Domain:
+                return pwmRequest.getBasePath() + "/";
+
+            case Private:
+                return pwmRequest.getBasePath() + PwmConstants.URL_PREFIX_PRIVATE;
+
+            case CurrentURL:
+                return pwmRequest.getURL().toString();
+
+            case PwmServlet:
+                return pwmRequest.getURL().determinePwmServletPath();
+
+            default:
+                throw new IllegalStateException( "undefined CookiePath type: " + this );
+        }
+
+    }
+}

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

@@ -23,12 +23,14 @@ package password.pwm.http;
 import com.google.gson.JsonParseException;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.ServletUtility;
 import password.pwm.util.Validator;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -46,7 +48,9 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 public class PwmHttpRequestWrapper
 {
@@ -89,10 +93,6 @@ public class PwmHttpRequestWrapper
         return acceptHeader.contains( PwmConstants.AcceptValue.html.getHeaderValue() ) || acceptHeader.contains( "*/*" );
     }
 
-    public String getContextPath( )
-    {
-        return httpServletRequest.getContextPath();
-    }
 
     public String readRequestBodyAsString( )
             throws IOException, PwmUnrecoverableException
@@ -315,26 +315,30 @@ public class PwmHttpRequestWrapper
         return Validator.sanitizeHeaderValue( appConfig, sanitizedInputValue );
     }
 
-    public Map<String, List<String>> readHeaderValuesMap( )
+    public List<String> readHeaderValuesAsString( final String headerName )
     {
         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<>();
-
-        for ( final Enumeration<String> headerNameEnum = req.getHeaderNames(); headerNameEnum.hasMoreElements(); )
+        final List<String> valueList = new ArrayList<>();
+        for ( final Enumeration<String> headerValueEnum = this.getHttpServletRequest().getHeaders( headerName ); headerValueEnum.hasMoreElements(); )
         {
-            final String headerName = headerNameEnum.nextElement();
-            final List<String> valueList = new ArrayList<>();
-            for ( final Enumeration<String> headerValueEnum = req.getHeaders( headerName ); headerValueEnum.hasMoreElements(); )
+            final String headerValue = headerValueEnum.nextElement();
+            final String sanitizedInputValue = Validator.sanitizeInputValue( appConfig, headerValue, maxChars );
+            final String sanitizedHeaderValue = Validator.sanitizeHeaderValue( appConfig, sanitizedInputValue );
+            if ( sanitizedHeaderValue != null && !sanitizedHeaderValue.isEmpty() )
             {
-                final String headerValue = headerValueEnum.nextElement();
-                final String sanitizedInputValue = Validator.sanitizeInputValue( appConfig, headerValue, maxChars );
-                final String sanitizedHeaderValue = Validator.sanitizeHeaderValue( appConfig, sanitizedInputValue );
-                if ( sanitizedHeaderValue != null && !sanitizedHeaderValue.isEmpty() )
-                {
-                    valueList.add( sanitizedHeaderValue );
-                }
+                valueList.add( sanitizedHeaderValue );
             }
+        }
+        return Collections.unmodifiableList( valueList );
+    }
+
+    public Map<String, List<String>> readHeaderValuesMap( )
+    {
+        final Map<String, List<String>> returnObj = new LinkedHashMap<>();
+
+        for ( final String headerName : headerNames() )
+        {
+            final List<String> valueList = readHeaderValuesAsString( headerName );
             if ( !valueList.isEmpty() )
             {
                 returnObj.put( headerName, Collections.unmodifiableList( valueList ) );
@@ -343,17 +347,24 @@ public class PwmHttpRequestWrapper
         return Collections.unmodifiableMap( returnObj );
     }
 
+    public List<String> headerNames( )
+    {
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+
+        return CollectionUtil.iteratorToStream( getHttpServletRequest().getHeaderNames().asIterator() )
+                .map( s -> Validator.sanitizeInputValue( appConfig, s, maxChars ) )
+                .collect( Collectors.toUnmodifiableList() );
+
+    }
+
     public List<String> parameterNames( )
     {
         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( appConfig, paramName, maxChars );
-            returnObj.add( returnName );
-        }
-        return Collections.unmodifiableList( returnObj );
+
+        return CollectionUtil.iteratorToStream( getHttpServletRequest().getParameterNames().asIterator() )
+                .map( s -> Validator.sanitizeInputValue( appConfig, s, maxChars ) )
+                .collect( Collectors.toUnmodifiableList() );
+
     }
 
     public Map<String, String> readParametersAsMap( )
@@ -381,7 +392,7 @@ public class PwmHttpRequestWrapper
         return Collections.unmodifiableMap( returnObj );
     }
 
-    public String readCookie( final String cookieName )
+    public Optional<String> readCookie( final String cookieName )
     {
         final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_COOKIE_MAX_READ_LENGTH ) );
         final Cookie[] cookies = this.getHttpServletRequest().getCookies();
@@ -393,12 +404,12 @@ public class PwmHttpRequestWrapper
                 {
                     final String rawCookieValue = cookie.getValue();
                     final String decodedCookieValue = StringUtil.urlDecode( rawCookieValue );
-                    return Validator.sanitizeInputValue( appConfig, decodedCookieValue, maxChars );
+                    return Optional.of( Validator.sanitizeInputValue( appConfig, decodedCookieValue, maxChars ) );
                 }
             }
         }
 
-        return null;
+        return Optional.empty();
     }
 
     private static String decodeStringToDefaultCharSet( final String input )
@@ -548,5 +559,15 @@ public class PwmHttpRequestWrapper
     {
         return isPrettyPrintJsonParameterTrue( this.getHttpServletRequest() );
     }
+
+    public static DomainID readDomainIdFromRequest(  final HttpServletRequest httpServletRequest ) throws PwmUnrecoverableException
+    {
+        final String domainIDStr = ( String ) httpServletRequest.getAttribute( PwmConstants.REQUEST_ATTR_DOMAIN );
+        if ( StringUtil.isEmpty( domainIDStr ) )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to read domainID in PwmRequest initialization" );
+        }
+        return DomainID.create( domainIDStr );
+    }
 }
 

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

@@ -21,17 +21,13 @@
 package password.pwm.http;
 
 import password.pwm.AppProperty;
-import password.pwm.PwmConstants;
 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;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -46,36 +42,6 @@ public class PwmHttpResponseWrapper
     private final HttpServletResponse httpServletResponse;
     private final AppConfig appConfig;
 
-    public enum CookiePath
-    {
-        Application,
-        Private,
-        CurrentURL,
-        PwmServlet,;
-
-        String toStringPath( final HttpServletRequest httpServletRequest )
-        {
-            switch ( this )
-            {
-                case Application:
-                    return httpServletRequest.getServletContext().getContextPath() + "/";
-
-                case Private:
-                    return httpServletRequest.getServletContext().getContextPath() + PwmConstants.URL_PREFIX_PRIVATE;
-
-                case CurrentURL:
-                    return httpServletRequest.getRequestURI();
-
-                case PwmServlet:
-                    return new PwmURL( httpServletRequest ).determinePwmServletPath();
-
-                default:
-                    throw new IllegalStateException( "undefined CookiePath type: " + this );
-            }
-
-        }
-    }
-
     public enum Flag
     {
         NonHttpOnly,
@@ -98,12 +64,6 @@ public class PwmHttpResponseWrapper
         return this.httpServletResponse;
     }
 
-    public void sendRedirect( final String url )
-            throws IOException
-    {
-        this.httpServletResponse.sendRedirect( Validator.sanitizeHeaderValue( appConfig, url ) );
-    }
-
     public boolean isCommitted( )
     {
         return this.httpServletResponse.isCommitted();
@@ -139,86 +99,13 @@ public class PwmHttpResponseWrapper
         return this.getHttpServletResponse().getOutputStream();
     }
 
-    public void writeCookie(
-            final String cookieName,
-            final String cookieValue,
-            final int seconds,
-            final Flag... flags
-    )
-    {
-        writeCookie( cookieName, cookieValue, seconds, null, flags );
-    }
-
-    public void writeCookie(
-            final String cookieName,
-            final String cookieValue,
-            final int seconds,
-            final CookiePath path,
-            final Flag... flags
-    )
-    {
-        if ( this.getHttpServletResponse().isCommitted() )
-        {
-            LOGGER.warn( () -> "attempt to write cookie '" + cookieName + "' after response is committed" );
-        }
-
-        final boolean secureFlag;
-        {
-            final String configValue = appConfig.readAppProperty( AppProperty.HTTP_COOKIE_DEFAULT_SECURE_FLAG );
-            if ( configValue == null || "auto".equalsIgnoreCase( configValue ) )
-            {
-                secureFlag = this.httpServletRequest.isSecure();
-            }
-            else
-            {
-                secureFlag = Boolean.parseBoolean( configValue );
-            }
-        }
-
-        final boolean httpOnlyEnabled = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
-        final boolean httpOnly = httpOnlyEnabled && !JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
-
-        final String value;
-        {
-            if ( cookieValue == null )
-            {
-                value = null;
-            }
-            else
-            {
-                if ( JavaHelper.enumArrayContainsValue( flags, Flag.BypassSanitation ) )
-                {
-                    value = StringUtil.urlEncode( cookieValue );
-                }
-                else
-                {
-                    value = StringUtil.urlEncode(
-                            Validator.sanitizeHeaderValue( appConfig, cookieValue )
-                    );
-                }
-            }
-        }
-
-        final Cookie theCookie = new Cookie( cookieName, value );
-        theCookie.setMaxAge( seconds >= -1 ? seconds : -1 );
-        theCookie.setHttpOnly( httpOnly );
-        theCookie.setSecure( secureFlag );
-
-        theCookie.setPath( path == null ? CookiePath.CurrentURL.toStringPath( httpServletRequest ) : path.toStringPath( httpServletRequest ) );
-        if ( value != null && value.length() > 2000 )
-        {
-            LOGGER.warn( () -> "writing large cookie to response: cookieName=" + cookieName + ", length=" + value.length() );
-        }
-        this.getHttpServletResponse().addCookie( theCookie );
-        addSameSiteCookieAttribute();
-    }
 
     void addSameSiteCookieAttribute( )
     {
         final PwmDomain pwmDomain;
         try
         {
-            pwmDomain = ContextManager.getPwmApplication( this.httpServletRequest ).getDefaultDomain();
+            pwmDomain = PwmRequest.forRequest( this.httpServletRequest, this.httpServletResponse ).getPwmDomain();
             final String value = pwmDomain.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_SAMESITE_VALUE );
             CookieManagementFilter.addSameSiteCookieAttribute( httpServletResponse, value );
         }
@@ -228,8 +115,5 @@ public class PwmHttpResponseWrapper
         }
     }
 
-    public void removeCookie( final String cookieName, final CookiePath path )
-    {
-        writeCookie( cookieName, null, 0, path );
-    }
+
 }

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

@@ -37,6 +37,15 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.AccountInformationProfile;
+import password.pwm.config.profile.ChangePasswordProfile;
+import password.pwm.config.profile.DeleteAccountProfile;
+import password.pwm.config.profile.HelpdeskProfile;
+import password.pwm.config.profile.PeopleSearchProfile;
+import password.pwm.config.profile.Profile;
+import password.pwm.config.profile.ProfileDefinition;
+import password.pwm.config.profile.SetupOtpProfile;
+import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -45,7 +54,6 @@ import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.PwmRequestID;
 import password.pwm.http.servlet.PwmServletDefinition;
-import password.pwm.http.servlet.command.CommandServlet;
 import password.pwm.ldap.UserInfo;
 import password.pwm.util.Validator;
 import password.pwm.util.java.LazySupplier;
@@ -86,54 +94,67 @@ public class PwmRequest extends PwmHttpRequestWrapper
     private final PwmRequestID pwmRequestID;
 
     private final transient PwmApplication pwmApplication;
-    private final transient PwmSession pwmSession;
     private final transient Supplier<SessionLabel> sessionLabelLazySupplier = new LazySupplier<>( this::makeSessionLabel );
 
     private final Set<PwmRequestFlag> flags = EnumSet.noneOf( PwmRequestFlag.class );
     private final Instant requestStartTime = Instant.now();
+    private final DomainID domainID;
     private final Lock cspCreationLock = new ReentrantLock();
 
+    private static final Lock CREATE_LOCK = new ReentrantLock();
+
     public static PwmRequest forRequest(
             final HttpServletRequest request,
             final HttpServletResponse response
     )
             throws PwmUnrecoverableException
     {
-        PwmRequest pwmRequest = ( PwmRequest ) request.getAttribute( PwmRequestAttribute.PwmRequest.toString() );
-        if ( pwmRequest == null )
+        CREATE_LOCK.lock();
+        try
         {
-            final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( request );
-            final PwmApplication pwmDomain = ContextManager.getPwmApplication( request );
-            pwmRequest = new PwmRequest( request, response, pwmDomain.getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER ), pwmSession );
-            request.setAttribute( PwmRequestAttribute.PwmRequest.toString(), pwmRequest );
+            PwmRequest pwmRequest = ( PwmRequest ) request.getAttribute( PwmRequestAttribute.PwmRequest.toString() );
+            if ( pwmRequest == null )
+            {
+                final PwmApplication pwmApplication = ContextManager.getPwmApplication( request );
+                pwmRequest = new PwmRequest( request, response, pwmApplication );
+                request.setAttribute( PwmRequestAttribute.PwmRequest.toString(), pwmRequest );
+            }
+            return pwmRequest;
+        }
+        finally
+        {
+            CREATE_LOCK.unlock();
         }
-        return pwmRequest;
     }
 
     private PwmRequest(
             final HttpServletRequest httpServletRequest,
             final HttpServletResponse httpServletResponse,
-            final PwmDomain pwmDomain,
-            final PwmSession pwmSession
+            final PwmApplication pwmApplication
     )
             throws PwmUnrecoverableException
     {
-        super( httpServletRequest, pwmDomain.getConfig().getAppConfig() );
+        super( httpServletRequest, pwmApplication.getConfig() );
         this.pwmRequestID = PwmRequestID.next();
-        this.pwmResponse = new PwmResponse( httpServletResponse, this, pwmDomain.getConfig() );
-        this.pwmSession = pwmSession;
-        this.pwmApplication = pwmDomain.getPwmApplication();
-        this.pwmURL = new PwmURL( this.getHttpServletRequest() );
+        this.pwmResponse = new PwmResponse( httpServletResponse, this, pwmApplication.getConfig() );
+        this.pwmApplication = pwmApplication;
+        this.pwmURL = PwmURL.create( this.getHttpServletRequest() );
+        this.domainID = PwmHttpRequestWrapper.readDomainIdFromRequest( httpServletRequest );
     }
 
     public PwmDomain getPwmDomain( )
     {
-        return pwmApplication.getDomains().get( getDomainID() );
+        return pwmApplication.domains().get( getDomainID() );
     }
 
     public PwmSession getPwmSession( )
     {
-        return pwmSession;
+        return getPwmSession( this.getPwmDomain() );
+    }
+
+    public PwmSession getPwmSession( final PwmDomain pwmDomain )
+    {
+        return PwmSessionFactory.readPwmSession( this.getHttpServletRequest().getSession(), pwmDomain );
     }
 
     public SessionLabel getLabel( )
@@ -163,7 +184,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
         {
             return PwmConstants.DEFAULT_LOCALE;
         }
-        return pwmSession.getSessionStateBean().getLocale();
+        return getPwmSession().getSessionStateBean().getLocale();
     }
 
     public void forwardToJsp( final JspUrl jspURL )
@@ -173,7 +194,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     }
 
     public void respondWithError( final ErrorInformation errorInformation )
-            throws IOException, ServletException
+            throws IOException, ServletException, PwmUnrecoverableException
     {
         respondWithError( errorInformation, true );
     }
@@ -182,7 +203,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
             final ErrorInformation errorInformation,
             final boolean forceLogout
     )
-            throws IOException, ServletException
+            throws IOException, ServletException, PwmUnrecoverableException
     {
         if ( forceLogout )
         {
@@ -194,30 +215,6 @@ public class PwmRequest extends PwmHttpRequestWrapper
         }
     }
 
-    public void sendRedirect( final String redirectURL )
-            throws PwmUnrecoverableException, IOException
-    {
-        getPwmResponse().sendRedirect( redirectURL );
-    }
-
-    public void sendRedirect( final PwmServletDefinition pwmServletDefinition )
-            throws PwmUnrecoverableException, IOException
-    {
-        getPwmResponse().sendRedirect( this.getContextPath() + pwmServletDefinition.servletUrl() );
-    }
-
-    public void sendRedirectToContinue( )
-            throws PwmUnrecoverableException, IOException
-    {
-        String redirectURL = this.getContextPath() + PwmServletDefinition.PublicCommand.servletUrl();
-        redirectURL = PwmURL.appendAndEncodeUrlParameters(
-                redirectURL,
-                Collections.singletonMap( PwmConstants.PARAM_ACTION_REQUEST, CommandServlet.CommandAction.next.toString() )
-        );
-        sendRedirect( redirectURL );
-    }
-
-
     public void outputJsonResult( final RestResultBean restResultBean )
             throws IOException
     {
@@ -230,7 +227,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return ContextManager.getContextManager( this );
     }
 
-    public InputStream readFileUploadStream( final String filePartName )
+    public Optional<InputStream> readFileUploadStream( final String filePartName )
             throws IOException, ServletException, PwmUnrecoverableException
     {
         try
@@ -248,7 +245,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
                     if ( filePartName.equals( item.getFieldName() ) )
                     {
-                        return item.openStream();
+                        return Optional.of( item.openStream() );
                     }
                 }
             }
@@ -257,14 +254,14 @@ public class PwmRequest extends PwmHttpRequestWrapper
         {
             LOGGER.error( () -> "error reading file upload: " + e.getMessage() );
         }
-        return null;
+        return Optional.empty();
     }
 
     public Map<String, FileUploadItem> readFileUploads(
             final int maxFileSize,
             final int maxItems
     )
-            throws IOException, ServletException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final Map<String, FileUploadItem> returnObj = new LinkedHashMap<>();
         try
@@ -376,7 +373,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
         redirectURL.append( PwmConstants.PARAM_TOKEN ).append( "=" ).append( tokenValue );
 
         LOGGER.debug( this, () -> "detected long servlet url, redirecting user to " + redirectURL );
-        sendRedirect( redirectURL.toString() );
+        getPwmResponse().sendRedirect( redirectURL.toString() );
         return true;
     }
 
@@ -400,14 +397,16 @@ public class PwmRequest extends PwmHttpRequestWrapper
     {
         if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
         {
-            final String debugTxt = debugHttpRequestToString( extraText, false );
+            final String moreExtraText = ( StringUtil.isEmpty( extraText ) ? "" : extraText + " " )
+                    + "request=" + this.getPwmRequestID() + ", domain=" + this.getDomainID().stringValue();
+            final String debugTxt = debugHttpRequestToString( moreExtraText, false );
             LOGGER.trace( this.getLabel(), () -> debugTxt, timeDuration );
         }
     }
 
     public boolean isAuthenticated( )
     {
-        return pwmSession.isAuthenticated();
+        return getPwmSession().isAuthenticated();
     }
 
     public boolean isForcedPageView( ) throws PwmUnrecoverableException
@@ -418,10 +417,9 @@ public class PwmRequest extends PwmHttpRequestWrapper
         }
 
         final PwmURL pwmURL = getURL();
-        final UserInfo userInfoBean = pwmSession.getUserInfo();
+        final UserInfo userInfoBean = getPwmSession().getUserInfo();
 
-
-        if ( pwmSession.getLoginInfoBean().isLoginFlag( LoginInfoBean.LoginFlag.forcePwChange ) && pwmURL.isChangePasswordURL() )
+        if ( getPwmSession().getLoginInfoBean().isLoginFlag( LoginInfoBean.LoginFlag.forcePwChange ) && pwmURL.isChangePasswordURL() )
         {
             return true;
         }
@@ -431,17 +429,17 @@ public class PwmRequest extends PwmHttpRequestWrapper
             return true;
         }
 
-        if ( userInfoBean.isRequiresResponseConfig() && pwmURL.isSetupResponsesURL() )
+        if ( userInfoBean.isRequiresResponseConfig() && pwmURL.matches( PwmServletDefinition.SetupResponses ) )
         {
             return true;
         }
 
-        if ( userInfoBean.isRequiresOtpConfig() && pwmURL.isSetupOtpSecretURL() )
+        if ( userInfoBean.isRequiresOtpConfig() && pwmURL.matches( PwmServletDefinition.SetupOtp ) )
         {
             return true;
         }
 
-        if ( userInfoBean.isRequiresUpdateProfile() && pwmURL.isProfileUpdateURL() )
+        if ( userInfoBean.isRequiresUpdateProfile() && pwmURL.matches( PwmServletDefinition.UpdateProfile ) )
         {
             return true;
         }
@@ -470,7 +468,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     {
         final LocalSessionStateBean ssBean = this.getPwmSession().getSessionStateBean();
         final String redirectURL = ssBean.getForwardURL();
-        return !StringUtil.isEmpty( redirectURL );
+        return StringUtil.notEmpty( redirectURL );
     }
 
     public String getForwardUrl( )
@@ -484,7 +482,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
         if ( StringUtil.isEmpty( redirectURL ) )
         {
-            redirectURL = this.getContextPath();
+            redirectURL = this.getBasePath();
         }
 
         if ( StringUtil.isEmpty( redirectURL ) )
@@ -503,6 +501,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     }
 
     public String getCspNonce( )
+            throws PwmUnrecoverableException
     {
         cspCreationLock.lock();
         try
@@ -525,15 +524,15 @@ public class PwmRequest extends PwmHttpRequestWrapper
     public <T extends Serializable> Optional<T> readEncryptedCookie( final String cookieName, final Class<T> returnClass )
             throws PwmUnrecoverableException
     {
-        final String strValue = this.readCookie( cookieName );
+        final Optional<String> strValue = this.readCookie( cookieName );
 
-        if ( StringUtil.isEmpty( strValue ) )
+        if ( strValue.isEmpty() )
         {
             return Optional.empty();
         }
 
-        final PwmSecurityKey pwmSecurityKey = pwmSession.getSecurityKey( this );
-        final T t = getPwmDomain().getSecureService().decryptObject( strValue, pwmSecurityKey, returnClass );
+        final PwmSecurityKey pwmSecurityKey = getPwmSession().getSecurityKey( this );
+        final T t = getPwmDomain().getSecureService().decryptObject( strValue.get(), pwmSecurityKey, returnClass );
         return Optional.of( t );
     }
 
@@ -575,6 +574,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     }
 
     public void invalidateSession( )
+            throws PwmUnrecoverableException
     {
         this.getPwmSession().unauthenticateUser( this );
         this.getHttpServletRequest().getSession().invalidate();
@@ -603,6 +603,23 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return false;
     }
 
+    public String getContextPath( )
+    {
+        return this.getHttpServletRequest().getContextPath();
+    }
+
+    public String getBasePath( )
+    {
+        final String rawContextPath = this.getHttpServletRequest().getContextPath();
+
+        if ( getAppConfig().isMultiDomain() )
+        {
+            return rawContextPath + "/" + this.getDomainID().stringValue();
+        }
+
+        return rawContextPath;
+    }
+
     public PwmRequestContext getPwmRequestContext()
     {
         return new PwmRequestContext( pwmApplication, this.getDomainID(), this.getLabel(), this.getLocale(), pwmRequestID );
@@ -620,7 +637,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
     public DomainID getDomainID()
     {
-        return PwmConstants.DOMAIN_ID_PLACEHOLDER;
+        return domainID;
     }
 
     public DomainConfig getDomainConfig()
@@ -632,4 +649,55 @@ public class PwmRequest extends PwmHttpRequestWrapper
     {
         return pwmApplication;
     }
+
+    private Profile getProfile( final PwmDomain pwmDomain, final ProfileDefinition profileDefinition ) throws PwmUnrecoverableException
+    {
+        if ( profileDefinition.isAuthenticated() && !getPwmSession().isAuthenticated() )
+        {
+            throw new IllegalStateException( "can not read authenticated profile while session is unauthenticated" );
+        }
+
+        final String profileID = getPwmSession().getUserInfo().getProfileIDs().get( profileDefinition );
+        if ( profileID != null )
+        {
+            return pwmDomain.getConfig().getProfileMap( profileDefinition ).get( profileID );
+        }
+        throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED );
+    }
+
+    public HelpdeskProfile getHelpdeskProfile() throws PwmUnrecoverableException
+    {
+        return ( HelpdeskProfile ) getProfile( getPwmDomain(), ProfileDefinition.Helpdesk );
+    }
+
+    public SetupOtpProfile getSetupOTPProfile() throws PwmUnrecoverableException
+    {
+        return ( SetupOtpProfile ) getProfile( getPwmDomain(), ProfileDefinition.SetupOTPProfile );
+    }
+
+    public UpdateProfileProfile getUpdateAttributeProfile() throws PwmUnrecoverableException
+    {
+        return ( UpdateProfileProfile ) getProfile( getPwmDomain(), ProfileDefinition.UpdateAttributes );
+    }
+
+    public PeopleSearchProfile getPeopleSearchProfile() throws PwmUnrecoverableException
+    {
+        return ( PeopleSearchProfile ) getProfile( getPwmDomain(), ProfileDefinition.PeopleSearch );
+    }
+
+    public DeleteAccountProfile getSelfDeleteProfile() throws PwmUnrecoverableException
+    {
+        return ( DeleteAccountProfile ) getProfile( getPwmDomain(), ProfileDefinition.DeleteAccount );
+    }
+
+    public ChangePasswordProfile getChangePasswordProfile() throws PwmUnrecoverableException
+    {
+        return ( ChangePasswordProfile ) getProfile( getPwmDomain(), ProfileDefinition.ChangePassword );
+    }
+
+    public AccountInformationProfile getAccountInfoProfile() throws PwmUnrecoverableException
+    {
+        return ( AccountInformationProfile ) getProfile( getPwmDomain(), ProfileDefinition.AccountInformation );
+    }
+
 }

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

@@ -43,7 +43,7 @@ public class PwmRequestContext
 
     public PwmDomain getPwmDomain()
     {
-        return getPwmApplication().getDomains().get( domainID );
+        return getPwmApplication().domains().get( domainID );
     }
 
     public DomainConfig getDomainConfig()

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

@@ -22,21 +22,27 @@ package password.pwm.http;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.apache.commons.lang3.exception.ExceptionUtils;
+import password.pwm.AppProperty;
+import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.http.servlet.command.CommandServlet;
 import password.pwm.i18n.Message;
+import password.pwm.util.Validator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.ws.server.RestResultBean;
 
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -45,6 +51,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Objects;
 import java.util.Set;
 
 public class PwmResponse extends PwmHttpResponseWrapper
@@ -81,10 +88,10 @@ public class PwmResponse extends PwmHttpResponseWrapper
     public PwmResponse(
             final HttpServletResponse response,
             final PwmRequest pwmRequest,
-            final DomainConfig domainConfig
+            final AppConfig appConfig
     )
     {
-        super( pwmRequest.getHttpServletRequest(), response, pwmRequest.getAppConfig() );
+        super( pwmRequest.getHttpServletRequest(), response, appConfig );
         this.pwmRequest = pwmRequest;
     }
 
@@ -137,7 +144,7 @@ public class PwmResponse extends PwmHttpResponseWrapper
         if ( showMessage )
         {
             LOGGER.trace( pwmRequest, () -> "skipping success page due to configuration setting" );
-            final String redirectUrl = pwmRequest.getContextPath()
+            final String redirectUrl = pwmRequest.getBasePath()
                     + PwmServletDefinition.PublicCommand.servletUrl()
                     + "?processAction=next";
             sendRedirect( redirectUrl );
@@ -158,7 +165,7 @@ public class PwmResponse extends PwmHttpResponseWrapper
             final ErrorInformation errorInformation,
             final Flag... flags
     )
-            throws IOException, ServletException
+            throws IOException, ServletException, PwmUnrecoverableException
     {
         LOGGER.error( pwmRequest.getLabel(), errorInformation );
 
@@ -203,7 +210,7 @@ public class PwmResponse extends PwmHttpResponseWrapper
             final boolean showDetail = pwmRequest.getPwmDomain().determineIfDetailErrorMsgShown();
             final String errorStatusText = showDetail
                     ? errorInformation.toDebugStr()
-                    : errorInformation.toUserStr( pwmRequest.getPwmSession(), pwmRequest.getPwmDomain() );
+                    : errorInformation.toUserStr( pwmRequest.getPwmSession(), pwmRequest.getDomainConfig() );
             getHttpServletResponse().sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorStatusText );
         }
 
@@ -225,13 +232,13 @@ public class PwmResponse extends PwmHttpResponseWrapper
     }
 
 
-    public void writeEncryptedCookie( final String cookieName, final Serializable cookieValue, final CookiePath path )
+    public void writeEncryptedCookie( final String cookieName, final Serializable cookieValue, final PwmCookiePath path )
             throws PwmUnrecoverableException
     {
         writeEncryptedCookie( cookieName, cookieValue, -1, path );
     }
 
-    public void writeEncryptedCookie( final String cookieName, final Serializable cookieValue, final int seconds, final CookiePath path )
+    public void writeEncryptedCookie( final String cookieName, final Serializable cookieValue, final int seconds, final PwmCookiePath path )
             throws PwmUnrecoverableException
     {
         final String jsonValue = JsonUtil.serialize( cookieValue );
@@ -246,24 +253,51 @@ public class PwmResponse extends PwmHttpResponseWrapper
         this.setContentType( contentType );
     }
 
-    @Override
     public void sendRedirect( final String url )
             throws IOException
     {
         sendRedirect( url, RedirectType.Found_302 );
     }
 
+    public void sendRedirectToIntoPage() throws IOException
+    {
+        final String redirectURL = pwmRequest.getPwmDomain().getConfig().readSettingAsString( PwmSetting.URL_INTRO );
+        sendRedirect( redirectURL );
+    }
+
+    public void sendRedirect( final PwmServletDefinition pwmServletDefinition )
+            throws PwmUnrecoverableException, IOException
+    {
+        sendRedirect( pwmRequest.getBasePath() + pwmServletDefinition.servletUrl() );
+    }
+
+    public void sendRedirectToContinue( )
+            throws PwmUnrecoverableException, IOException
+    {
+        String redirectURL = pwmRequest.getBasePath() + PwmServletDefinition.PublicCommand.servletUrl();
+        redirectURL = PwmURL.appendAndEncodeUrlParameters(
+                redirectURL,
+                Collections.singletonMap( PwmConstants.PARAM_ACTION_REQUEST, CommandServlet.CommandAction.next.toString() )
+        );
+        sendRedirect( redirectURL );
+    }
+
     public void sendRedirect( final String url, final RedirectType redirectType )
             throws IOException
     {
+        Objects.requireNonNull ( url );
         preCommitActions();
 
-        final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
-        resp.setStatus( redirectType.getCode() );
+        final String basePath = pwmRequest.getBasePath();
+        final String effectiveUrl = url.startsWith( basePath )
+                ? url
+                : basePath + url;
 
         // http "other" redirect
-        resp.setHeader( HttpHeader.Location.getHttpName(), url );
-        LOGGER.trace( pwmRequest, () -> "sending " + redirectType.getCode() + " redirect to " + url );
+        final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
+        resp.setStatus( redirectType.getCode() );
+        resp.setHeader( HttpHeader.Location.getHttpName(), effectiveUrl.toString() );
+        LOGGER.trace( pwmRequest, () -> "sending " + redirectType.getCode() + " redirect to " + effectiveUrl.toString() );
     }
 
     private void preCommitActions( )
@@ -288,4 +322,80 @@ public class PwmResponse extends PwmHttpResponseWrapper
     {
         pwmResponseFlags.add( flag );
     }
+
+    public void writeCookie(
+            final String cookieName,
+            final String cookieValue,
+            final int seconds,
+            final PwmCookiePath path,
+            final PwmHttpResponseWrapper.Flag... flags
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( this.getHttpServletResponse().isCommitted() )
+        {
+            LOGGER.warn( () -> "attempt to write cookie '" + cookieName + "' after response is committed" );
+        }
+
+        final AppConfig appConfig = pwmRequest.getAppConfig();
+
+        final boolean secureFlag;
+        {
+            final String configValue = appConfig.readAppProperty( AppProperty.HTTP_COOKIE_DEFAULT_SECURE_FLAG );
+            if ( configValue == null || "auto".equalsIgnoreCase( configValue ) )
+            {
+                secureFlag = pwmRequest.getHttpServletRequest().isSecure();
+            }
+            else
+            {
+                secureFlag = Boolean.parseBoolean( configValue );
+            }
+        }
+
+        final boolean httpOnlyEnabled = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
+        final boolean httpOnly = httpOnlyEnabled && !JavaHelper.enumArrayContainsValue( flags, PwmHttpResponseWrapper.Flag.NonHttpOnly );
+
+        final String value;
+        {
+            if ( cookieValue == null )
+            {
+                value = null;
+            }
+            else
+            {
+                if ( JavaHelper.enumArrayContainsValue( flags, PwmHttpResponseWrapper.Flag.BypassSanitation ) )
+                {
+                    value = StringUtil.urlEncode( cookieValue );
+                }
+                else
+                {
+                    value = StringUtil.urlEncode(
+                            Validator.sanitizeHeaderValue( appConfig, cookieValue )
+                    );
+                }
+            }
+        }
+
+        final Cookie theCookie = new Cookie( cookieName, value );
+        theCookie.setMaxAge( JavaHelper.rangeCheck( -1, Integer.MAX_VALUE, seconds ) );
+        theCookie.setHttpOnly( httpOnly );
+        theCookie.setSecure( secureFlag );
+
+        theCookie.setPath( path == null
+                ? PwmCookiePath.CurrentURL.toStringPath( pwmRequest )
+                : path.toStringPath( pwmRequest ) );
+        if ( value != null && value.length() > 2000 )
+        {
+            LOGGER.warn( () -> "writing large cookie to response: cookieName=" + cookieName + ", length=" + value.length() );
+        }
+        this.getHttpServletResponse().addCookie( theCookie );
+        addSameSiteCookieAttribute();
+    }
+
+    public void removeCookie( final String cookieName, final PwmCookiePath path )
+            throws PwmUnrecoverableException
+    {
+        writeCookie( cookieName, null, 0, path );
+    }
+
 }

+ 14 - 15
server/src/main/java/password/pwm/http/PwmSession.java

@@ -21,9 +21,11 @@
 package password.pwm.http;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import lombok.Data;
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
 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;
@@ -52,26 +54,27 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * @author Jason D. Rivard
  */
+@Data
 public class PwmSession implements Serializable
 {
     private static final long serialVersionUID = 1L;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
 
-    private final transient PwmDomain pwmDomain;
-
     @SuppressFBWarnings( "SE_TRANSIENT_FIELD_NOT_RESTORED" )
     private final transient LocalSessionStateBean sessionStateBean = new LocalSessionStateBean();
 
     @SuppressFBWarnings( "SE_TRANSIENT_FIELD_NOT_RESTORED" )
     private final transient UserSessionDataCacheBean userSessionDataCacheBean = new UserSessionDataCacheBean();
 
+    private final DomainID domainID;
     private LoginInfoBean loginInfoBean;
     private transient UserInfo userInfo;
 
@@ -81,7 +84,6 @@ public class PwmSession implements Serializable
     private final transient SessionManager sessionManager;
 
     public static PwmSession createPwmSession( final PwmDomain pwmDomain )
-            throws PwmUnrecoverableException
     {
         CREATION_LOCK.lock();
         try
@@ -95,14 +97,10 @@ public class PwmSession implements Serializable
     }
 
     private PwmSession( final PwmDomain pwmDomain )
-            throws PwmUnrecoverableException
     {
-        if ( pwmDomain == null )
-        {
-            throw new IllegalStateException( "PwmApplication must be available during session creation" );
-        }
+        Objects.requireNonNull( pwmDomain );
+        this.domainID = pwmDomain.getDomainID();
 
-        this.pwmDomain = pwmDomain;
         this.sessionStateBean.setSessionID( pwmDomain.getSessionTrackService().generateNewSessionID() );
 
         this.sessionStateBean.setSessionLastAccessedTime( Instant.now() );
@@ -158,7 +156,7 @@ public class PwmSession implements Serializable
         if ( getLoginInfoBean().getAuthFlags().contains( AuthenticationType.AUTH_BIND_INHIBIT ) )
         {
             userInfo = UserInfoFactory.newUserInfo(
-                    pwmDomain,
+                    pwmRequest.getPwmApplication(),
                     pwmRequest.getLabel(),
                     getSessionStateBean().getLocale(),
                     oldUserInfoBean.getUserIdentity(),
@@ -168,7 +166,7 @@ public class PwmSession implements Serializable
         else
         {
             userInfo = UserInfoFactory.newUserInfoUsingProxy(
-                    pwmDomain,
+                    pwmRequest.getPwmApplication(),
                     pwmRequest.getLabel(),
                     oldUserInfoBean.getUserIdentity(),
                     getSessionStateBean().getLocale(),
@@ -245,6 +243,7 @@ public class PwmSession implements Serializable
      * @param pwmRequest current request of the user
      */
     public void unauthenticateUser( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
     {
         final LocalSessionStateBean ssBean = getSessionStateBean();
 
@@ -275,7 +274,7 @@ public class PwmSession implements Serializable
 
             final String nonceCookieName = pwmRequest.getDomainConfig().readAppProperty( AppProperty.HTTP_COOKIE_NONCE_NAME );
             pwmRequest.setAttribute( PwmRequestAttribute.CookieNonce, null );
-            pwmRequest.getPwmResponse().removeCookie( nonceCookieName, PwmHttpResponseWrapper.CookiePath.Application );
+            pwmRequest.getPwmResponse().removeCookie( nonceCookieName, PwmCookiePath.Domain );
 
             try
             {
@@ -384,7 +383,7 @@ public class PwmSession implements Serializable
             String nonce = ( String ) pwmRequest.getAttribute( PwmRequestAttribute.CookieNonce );
             if ( nonce == null || nonce.length() < length )
             {
-                nonce = pwmRequest.readCookie( cookieName );
+                nonce = pwmRequest.readCookie( cookieName ).orElse( null );
             }
 
             boolean newNonce = false;
@@ -408,7 +407,7 @@ public class PwmSession implements Serializable
             if ( newNonce )
             {
                 pwmRequest.setAttribute( PwmRequestAttribute.CookieNonce, nonce );
-                pwmRequest.getPwmResponse().writeCookie( cookieName, nonce, -1, PwmHttpResponseWrapper.CookiePath.Application );
+                pwmRequest.getPwmResponse().writeCookie( cookieName, nonce, -1, PwmCookiePath.Domain );
             }
 
             return pwmSecurityKey;

+ 25 - 22
server/src/main/java/password/pwm/http/PwmSessionWrapper.java → server/src/main/java/password/pwm/http/PwmSessionFactory.java

@@ -20,24 +20,22 @@
 
 package password.pwm.http;
 
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
+import password.pwm.PwmDomain;
+import password.pwm.bean.DomainID;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
-import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
-public class PwmSessionWrapper
+public class PwmSessionFactory
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSessionWrapper.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSessionFactory.class );
 
-    private transient PwmSession pwmSession;
-
-    private PwmSessionWrapper( )
+    private PwmSessionFactory( )
     {
 
     }
@@ -54,24 +52,29 @@ public class PwmSessionWrapper
         setHttpSessionIdleTimeout( pwmDomain, pwmSession, httpSession );
     }
 
+    private static PwmSession createSession( final PwmDomain pwmDomain )
+    {
+        // handle pwmSession init and assignment.
 
-    public static PwmSession readPwmSession( final HttpSession httpSession )
-            throws PwmUnrecoverableException
+        return PwmSession.createPwmSession( pwmDomain );
+    }
+
+
+    public static PwmSession readPwmSession( final HttpSession httpSession, final PwmDomain pwmdomain )
     {
-        final PwmSession returnSession = ( PwmSession ) httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION );
-        if ( returnSession == null )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "attempt to read PwmSession from HttpSession failed" ) );
-        }
-        return returnSession;
+        final Map<DomainID, PwmSession> map = getDomainSessionMap( httpSession );
+        return map.computeIfAbsent( pwmdomain.getDomainID(), k -> createSession( pwmdomain ) );
     }
 
-    public static PwmSession readPwmSession(
-            final HttpServletRequest httpRequest
-    )
-            throws PwmUnrecoverableException
+    public static Map<DomainID, PwmSession> getDomainSessionMap( final HttpSession httpSession )
     {
-        return readPwmSession( httpRequest.getSession() );
+        Map<DomainID, PwmSession> map = ( Map<DomainID, PwmSession> ) httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION );
+        if ( map == null )
+        {
+            map = new ConcurrentHashMap<>();
+            httpSession.setAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION, map );
+        }
+        return map;
     }
 
     public static void setHttpSessionIdleTimeout(

+ 101 - 97
server/src/main/java/password/pwm/http/PwmURL.java

@@ -22,6 +22,8 @@ package password.pwm.http;
 
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
+import password.pwm.config.AppConfig;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -30,6 +32,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -43,22 +46,18 @@ public class PwmURL
 
     private final URI uri;
     private final String contextPath;
+    private final AppConfig appConfig;
 
-    public PwmURL(
+    private PwmURL(
             final URI uri,
-            final String contextPath
+            final String contextPath,
+            final AppConfig appConfig
     )
     {
-        Objects.requireNonNull( uri );
-        this.uri = uri.normalize();
-        this.contextPath = contextPath;
-    }
 
-    public PwmURL(
-            final HttpServletRequest req
-    )
-    {
-        this( URI.create( req.getRequestURL().toString() ), req.getContextPath() );
+        this.uri = Objects.requireNonNull( uri ).normalize();
+        this.contextPath = Objects.requireNonNull( contextPath );
+        this.appConfig = Objects.requireNonNull( appConfig );
     }
 
     /**
@@ -102,137 +101,113 @@ public class PwmURL
         return true;
     }
 
-    public boolean isLoginServlet( )
-    {
-        return isPwmServletURL( PwmServletDefinition.Login );
-    }
-
-    public boolean isResourceURL( )
-    {
-        return checkIfStartsWithURL( PwmConstants.URL_PREFIX_PUBLIC + "/resources/" ) || isReferenceURL();
-    }
-
-    public boolean isReferenceURL( )
-    {
-        return checkIfMatchesURL( PwmConstants.URL_PREFIX_PUBLIC + "/reference" ) || checkIfStartsWithURL( PwmConstants.URL_PREFIX_PUBLIC + "/reference/" );
-    }
-
-    public boolean isLogoutURL( )
+    public static PwmURL create(
+            final HttpServletRequest req
+    )
+            throws PwmUnrecoverableException
     {
-        return isPwmServletURL( PwmServletDefinition.Logout );
+        return new PwmURL(
+                URI.create( req.getRequestURL().toString() ),
+                req.getContextPath(),
+                ContextManager.getPwmApplication( req ).getConfig() );
     }
 
-    public boolean isForgottenPasswordServlet( )
+    public static PwmURL create(
+            final HttpServletRequest req,
+            final AppConfig appConfig
+    )
     {
-        return isPwmServletURL( PwmServletDefinition.ForgottenPassword );
+        return new PwmURL(
+                URI.create( req.getRequestURL().toString() ),
+                req.getContextPath(),
+                appConfig );
     }
 
-    public boolean isForgottenUsernameServlet( )
+    public static PwmURL create(
+            final URI uri,
+            final String contextPath,
+            final AppConfig appConfig
+    )
+            throws PwmUnrecoverableException
     {
-        return isPwmServletURL( PwmServletDefinition.ForgottenUsername );
+        return new PwmURL( uri, contextPath, appConfig );
     }
 
-    public boolean isUserActivationServlet( )
+    public boolean isResourceURL( )
     {
-        return isPwmServletURL( PwmServletDefinition.ActivateUser );
+        return checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PUBLIC + "/resources/" ) ) || isReferenceURL();
     }
 
-    public boolean isNewUserRegistrationServlet( )
+    public boolean isReferenceURL( )
     {
-        return isPwmServletURL( PwmServletDefinition.NewUser );
+        return checkIfMatchesURL(
+                List.of( PwmConstants.URL_PREFIX_PUBLIC + "/reference" ) )
+                || checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PUBLIC + "/reference/" ) );
     }
 
-    public boolean isOauthConsumer( )
-    {
-        return isPwmServletURL( PwmServletDefinition.OAuthConsumer );
-    }
 
     public boolean isPrivateUrl( )
     {
-        return checkIfStartsWithURL( PwmConstants.URL_PREFIX_PRIVATE + "/" );
+        return checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PRIVATE + "/" ) );
     }
 
     public boolean isAdminUrl( )
     {
-        return isPwmServletURL( PwmServletDefinition.Admin );
+        return matches( PwmServletDefinition.Admin );
     }
 
     public boolean isIndexPage( )
     {
-        return checkIfMatchesURL(
+        return checkIfMatchesURL( List.of(
                 "",
                 "/",
                 PwmConstants.URL_PREFIX_PRIVATE,
                 PwmConstants.URL_PREFIX_PUBLIC,
                 PwmConstants.URL_PREFIX_PRIVATE + "/",
                 PwmConstants.URL_PREFIX_PUBLIC + "/"
-        );
+        ) );
     }
 
     public boolean isPublicUrl( )
     {
-        return checkIfStartsWithURL( PwmConstants.URL_PREFIX_PUBLIC + "/" );
+        return checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PUBLIC + "/" ) );
     }
 
     public boolean isCommandServletURL( )
     {
-        return isPwmServletURL( PwmServletDefinition.PublicCommand )
-                || isPwmServletURL( PwmServletDefinition.PrivateCommand );
+        return matches( PwmServletDefinition.PublicCommand )
+                || matches( PwmServletDefinition.PrivateCommand );
 
     }
 
     public boolean isRestService( )
     {
-        return checkIfStartsWithURL( PwmConstants.URL_PREFIX_PUBLIC + "/rest/" );
+        return checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PUBLIC + "/rest/" ) );
 
     }
 
     public boolean isConfigManagerURL( )
     {
-        return checkIfStartsWithURL( PwmConstants.URL_PREFIX_PRIVATE + "/config/" );
-    }
-
-    public boolean isClientApiServlet( )
-    {
-        return isPwmServletURL( PwmServletDefinition.ClientApi );
+        return checkIfStartsWithURL( List.of( PwmConstants.URL_PREFIX_PRIVATE + "/config/" ) );
     }
 
     public boolean isConfigGuideURL( )
     {
-        return isPwmServletURL( PwmServletDefinition.ConfigGuide );
+        return matches( PwmServletDefinition.ConfigGuide );
     }
 
-    public boolean isPwmServletURL( final PwmServletDefinition pwmServletDefinition )
-    {
-        return checkIfStartsWithURL( pwmServletDefinition.urlPatterns() );
-    }
 
     public boolean isChangePasswordURL( )
     {
-        return isPwmServletURL( PwmServletDefinition.PrivateChangePassword )
-                || isPwmServletURL( PwmServletDefinition.PublicChangePassword );
-    }
-
-    public boolean isSetupResponsesURL( )
-    {
-        return isPwmServletURL( PwmServletDefinition.SetupResponses );
-    }
-
-    public boolean isSetupOtpSecretURL( )
-    {
-        return isPwmServletURL( PwmServletDefinition.SetupOtp );
-    }
-
-    public boolean isProfileUpdateURL( )
-    {
-        return isPwmServletURL( PwmServletDefinition.UpdateProfile );
+        return matches( PwmServletDefinition.PrivateChangePassword )
+                || matches( PwmServletDefinition.PublicChangePassword );
     }
 
     public Optional<PwmServletDefinition> forServletDefinition()
     {
         for ( final PwmServletDefinition pwmServletDefinition : PwmServletDefinition.values() )
         {
-            if ( isPwmServletURL( pwmServletDefinition ) )
+            if ( checkIfStartsWithURL( pwmServletDefinition.urlPatterns() ) )
             {
                 return Optional.of( pwmServletDefinition );
             }
@@ -240,6 +215,17 @@ public class PwmURL
         return Optional.empty();
     }
 
+    public boolean matches( final PwmServletDefinition servletDefinition )
+    {
+        return matches( Collections.singleton( servletDefinition ) );
+    }
+
+    public boolean matches( final Collection<PwmServletDefinition> servletDefinitions )
+    {
+        final Optional<PwmServletDefinition> foundDefinition = forServletDefinition();
+        return foundDefinition.isPresent() && servletDefinitions.contains( foundDefinition.get() );
+    }
+
     public boolean isLocalizable( )
     {
         return !isConfigGuideURL()
@@ -253,17 +239,13 @@ public class PwmURL
         return uri.toString();
     }
 
-    private boolean checkIfStartsWithURL( final String... url )
+    private boolean checkIfStartsWithURL( final List<String> url )
     {
-        final String servletRequestPath = uri.getPath();
-        if ( servletRequestPath == null )
-        {
-            return false;
-        }
+        final String servletRequestPath = pathMinusContextAndDomain();
 
         for ( final String loopURL : url )
         {
-            if ( servletRequestPath.startsWith( contextPath + loopURL ) )
+            if ( servletRequestPath.startsWith( loopURL ) )
             {
                 return true;
             }
@@ -272,18 +254,13 @@ public class PwmURL
         return false;
     }
 
-    private boolean checkIfMatchesURL( final String... url )
+    private boolean checkIfMatchesURL( final List<String> url )
     {
-        final String servletRequestPath = uri.getPath();
-        if ( servletRequestPath == null )
-        {
-            return false;
-        }
+        final String servletRequestPath = pathMinusContextAndDomain();
 
         for ( final String loopURL : url )
         {
-            final String testURL = contextPath + loopURL;
-            if ( servletRequestPath.equals( testURL ) )
+            if ( servletRequestPath.equals( loopURL ) )
             {
                 return true;
             }
@@ -292,6 +269,11 @@ public class PwmURL
         return false;
     }
 
+    public List<String> splitPaths()
+    {
+        return splitPathString( this.uri.getPath() );
+    }
+
     public static List<String> splitPathString( final String input )
     {
         if ( input == null )
@@ -418,26 +400,48 @@ public class PwmURL
 
     public String determinePwmServletPath( )
     {
-        final String requestPath = this.uri.getPath();
+        final String requestPath = this.pathMinusContextAndDomain();
         for ( final PwmServletDefinition servletDefinition : PwmServletDefinition.values() )
         {
             for ( final String pattern : servletDefinition.urlPatterns() )
             {
-                final String testPath = contextPath + pattern;
-                if ( requestPath.startsWith( testPath ) )
+                if ( requestPath.startsWith( pattern ) )
                 {
-                    return testPath;
+                    return pattern;
                 }
             }
         }
         return requestPath;
     }
 
+    private String pathMinusContextAndDomain()
+    {
+        String path = this.uri.getPath();
+        if ( path.startsWith( this.contextPath ) )
+        {
+            path = path.substring( this.contextPath.length() );
+        }
+
+        if ( appConfig.isMultiDomain() )
+        {
+            for ( final String domain : appConfig.getDomainIDs() )
+            {
+                final String testPath = '/' + domain;
+                if ( path.startsWith( testPath ) )
+                {
+                    return path.substring( testPath.length() );
+                }
+            }
+        }
+
+        return path;
+    }
+
     public static boolean testIfUrlMatchesAllowedPattern(
             final String testURI,
             final List<String> whiteList,
             final SessionLabel sessionLabel
-            )
+    )
     {
         final String regexPrefix = "regex:";
         for ( final String loopFragment : whiteList )

+ 17 - 66
server/src/main/java/password/pwm/http/SessionManager.java

@@ -26,24 +26,16 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.Permission;
 import password.pwm.PwmDomain;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.profile.AccountInformationProfile;
-import password.pwm.config.profile.ChangePasswordProfile;
-import password.pwm.config.profile.DeleteAccountProfile;
-import password.pwm.config.profile.HelpdeskProfile;
-import password.pwm.config.profile.PeopleSearchProfile;
-import password.pwm.config.profile.Profile;
-import password.pwm.config.profile.ProfileDefinition;
-import password.pwm.config.profile.SetupOtpProfile;
-import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.LdapOperationsHelper;
-import password.pwm.ldap.permission.UserPermissionUtility;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.AuthenticationType;
+import password.pwm.ldap.permission.UserPermissionUtility;
 import password.pwm.util.PasswordData;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
@@ -106,10 +98,11 @@ public class SessionManager
 
         try
         {
+            final AppConfig appConfig = pwmDomain.getConfig().getAppConfig();
             this.chaiProvider = LdapOperationsHelper.createChaiProvider(
                     pwmDomain,
                     pwmSession.getLabel(),
-                    userIdentity.getLdapProfile( pwmDomain.getPwmApplication().getConfig() ),
+                    userIdentity.getLdapProfile( appConfig ),
                     pwmDomain.getConfig(),
                     userIdentity.getUserDN(),
                     userPassword
@@ -224,10 +217,18 @@ public class SessionManager
                         () -> String.format( "checking permission %s for user %s", permission.toString(), pwmSession.getUserInfo().getUserIdentity().toDelimitedKey() ) );
             }
 
-            final PwmSetting setting = permission.getPwmSetting();
-            final List<UserPermission> userPermission = pwmDomain.getConfig().readSettingAsUserPermission( setting );
-            final boolean result = UserPermissionUtility.testUserPermission( pwmDomain, pwmSession.getLabel(), pwmSession.getUserInfo().getUserIdentity(), userPermission );
-            status = result ? Permission.PermissionStatus.GRANTED : Permission.PermissionStatus.DENIED;
+            if ( permission == Permission.PWMADMIN && !pwmDomain.getConfig().isAdministrativeDomain() )
+            {
+                status = Permission.PermissionStatus.DENIED;
+            }
+            else
+            {
+                final PwmSetting setting = permission.getPwmSetting();
+                final List<UserPermission> userPermission = pwmDomain.getConfig().readSettingAsUserPermission( setting );
+                final boolean result = UserPermissionUtility.testUserPermission( pwmDomain, pwmSession.getLabel(), pwmSession.getUserInfo().getUserIdentity(), userPermission );
+                status = result ? Permission.PermissionStatus.GRANTED : Permission.PermissionStatus.DENIED;
+            }
+
             pwmSession.getUserSessionDataCacheBean().setPermission( permission, status );
 
             {
@@ -250,56 +251,6 @@ public class SessionManager
         final UserInfo userInfoBean = pwmSession.isAuthenticated()
                 ? pwmSession.getUserInfo()
                 : null;
-        return MacroRequest.forUser( pwmDomain, pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
-    }
-
-    public Profile getProfile( final PwmDomain pwmDomain, final ProfileDefinition profileDefinition ) throws PwmUnrecoverableException
-    {
-        if ( profileDefinition.isAuthenticated() && !pwmSession.isAuthenticated() )
-        {
-            throw new IllegalStateException( "can not read authenticated profile while session is unauthenticated" );
-        }
-
-        final String profileID = pwmSession.getUserInfo().getProfileIDs().get( profileDefinition );
-        if ( profileID != null )
-        {
-            return pwmDomain.getConfig().getProfileMap( profileDefinition ).get( profileID );
-        }
-        throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED );
-    }
-
-    public HelpdeskProfile getHelpdeskProfile() throws PwmUnrecoverableException
-    {
-        return ( HelpdeskProfile ) getProfile( pwmDomain, ProfileDefinition.Helpdesk );
-    }
-
-    public SetupOtpProfile getSetupOTPProfile() throws PwmUnrecoverableException
-    {
-        return ( SetupOtpProfile ) getProfile( pwmDomain, ProfileDefinition.SetupOTPProfile );
-    }
-
-    public UpdateProfileProfile getUpdateAttributeProfile() throws PwmUnrecoverableException
-    {
-        return ( UpdateProfileProfile ) getProfile( pwmDomain, ProfileDefinition.UpdateAttributes );
-    }
-
-    public PeopleSearchProfile getPeopleSearchProfile() throws PwmUnrecoverableException
-    {
-        return ( PeopleSearchProfile ) getProfile( pwmDomain, ProfileDefinition.PeopleSearch );
-    }
-
-    public DeleteAccountProfile getSelfDeleteProfile() throws PwmUnrecoverableException
-    {
-        return ( DeleteAccountProfile ) getProfile( pwmDomain, ProfileDefinition.DeleteAccount );
-    }
-
-    public ChangePasswordProfile getChangePasswordProfile() throws PwmUnrecoverableException
-    {
-        return ( ChangePasswordProfile ) getProfile( pwmDomain, ProfileDefinition.ChangePassword );
-    }
-
-    public AccountInformationProfile getAccountInfoProfile() throws PwmUnrecoverableException
-    {
-        return ( AccountInformationProfile ) getProfile( pwmDomain, ProfileDefinition.AccountInformation );
+        return MacroRequest.forUser( pwmDomain.getPwmApplication(), pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
     }
 }

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

@@ -27,7 +27,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.ProcessStatus;
-import password.pwm.http.PwmHttpResponseWrapper;
+import password.pwm.http.PwmCookiePath;
 import password.pwm.http.PwmRequest;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.util.logging.PwmLogger;
@@ -45,7 +45,8 @@ public abstract class HttpAuthenticationUtilities
     private static final Set<AuthenticationMethod> IGNORED_AUTH_METHODS = EnumSet.noneOf( AuthenticationMethod.class );
 
 
-    public static ProcessStatus attemptAuthenticationMethods( final PwmRequest pwmRequest ) throws IOException, ServletException
+    public static ProcessStatus attemptAuthenticationMethods( final PwmRequest pwmRequest )
+            throws IOException, ServletException, PwmUnrecoverableException
     {
         if ( pwmRequest.isAuthenticated() )
         {
@@ -151,7 +152,7 @@ public abstract class HttpAuthenticationUtilities
 
         try
         {
-            pwmRequest.getPwmResponse().writeEncryptedCookie( cookieName, httpAuthRecord, cookieAgeSeconds, PwmHttpResponseWrapper.CookiePath.Application );
+            pwmRequest.getPwmResponse().writeEncryptedCookie( cookieName, httpAuthRecord, cookieAgeSeconds, PwmCookiePath.Domain );
             LOGGER.debug( pwmRequest, () -> "wrote auth record cookie to user browser for use during forgotten password" );
         }
         catch ( final PwmUnrecoverableException e )

+ 2 - 10
server/src/main/java/password/pwm/http/filter/AbstractPwmFilter.java

@@ -62,7 +62,7 @@ public abstract class AbstractPwmFilter implements Filter
         final boolean interested;
         try
         {
-            final PwmURL pwmURL = new PwmURL( req );
+            final PwmURL pwmURL = PwmURL.create( req );
             interested = isInterested( mode, pwmURL );
         }
         catch ( final Exception e )
@@ -78,16 +78,10 @@ public abstract class AbstractPwmFilter implements Filter
             try
             {
                 pwmRequest = PwmRequest.forRequest( req, resp );
+                final PwmURL pwmURL = PwmURL.create( req );
             }
             catch ( final PwmException e )
             {
-                final PwmURL pwmURL = new PwmURL( req );
-                if ( pwmURL.isResourceURL() )
-                {
-                    filterChain.doFilter( req, resp );
-                    return;
-                }
-
                 LOGGER.error( pwmRequest, () -> "unexpected error processing filter chain: " + e.getMessage(), e );
             }
 
@@ -104,13 +98,11 @@ public abstract class AbstractPwmFilter implements Filter
             {
                 LOGGER.debug( pwmRequest, () -> "i/o error processing request: " + e.getMessage() );
             }
-
         }
         else
         {
             filterChain.doFilter( req, resp );
         }
-
     }
 
     abstract void processFilter(

Некоторые файлы не были показаны из-за большого количества измененных файлов