Selaa lähdekoodia

introduce domainID value to various key classes

Jason Rivard 4 vuotta sitten
vanhempi
commit
9d089f7217
44 muutettua tiedostoa jossa 609 lisäystä ja 498 poistoa
  1. 1 0
      pom.xml
  2. 2 2
      server/src/main/java/password/pwm/PwmApplication.java
  3. 3 0
      server/src/main/java/password/pwm/PwmConstants.java
  4. 48 29
      server/src/main/java/password/pwm/bean/UserIdentity.java
  5. 1 1
      server/src/main/java/password/pwm/config/Configuration.java
  6. 17 60
      server/src/main/java/password/pwm/config/PwmSetting.java
  7. 96 102
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  8. 27 0
      server/src/main/java/password/pwm/config/PwmSettingScope.java
  9. 66 0
      server/src/main/java/password/pwm/config/PwmSettingStats.java
  10. 3 1
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  11. 6 3
      server/src/main/java/password/pwm/config/SettingReader.java
  12. 2 1
      server/src/main/java/password/pwm/config/profile/AbstractProfile.java
  13. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  14. 1 1
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  15. 22 16
      server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java
  16. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  17. 15 6
      server/src/main/java/password/pwm/config/value/data/UserPermission.java
  18. 4 1
      server/src/main/java/password/pwm/health/HealthMessage.java
  19. 4 6
      server/src/main/java/password/pwm/health/HealthMonitor.java
  20. 39 48
      server/src/main/java/password/pwm/health/HealthRecord.java
  21. 18 15
      server/src/main/java/password/pwm/health/HealthStatus.java
  22. 3 3
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  23. 2 7
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  24. 6 0
      server/src/main/java/password/pwm/http/PwmRequest.java
  25. 1 1
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  26. 1 1
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java
  27. 11 11
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerCertificatesServlet.java
  28. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  29. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  30. 1 1
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  31. 4 1
      server/src/main/java/password/pwm/ldap/search/UserSearchJob.java
  32. 2 2
      server/src/main/java/password/pwm/svc/event/DatabaseUserHistory.java
  33. 2 2
      server/src/main/java/password/pwm/svc/event/LdapXmlUserHistory.java
  34. 2 50
      server/src/main/java/password/pwm/util/BasicAuthInfo.java
  35. 2 2
      server/src/main/java/password/pwm/util/SampleDataGenerator.java
  36. 1 1
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  37. 14 1
      server/src/main/java/password/pwm/util/logging/PwmLogEvent.java
  38. 1 1
      server/src/main/java/password/pwm/util/password/PasswordUtility.java
  39. 109 109
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  40. 11 1
      server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java
  41. 2 2
      server/src/test/java/password/pwm/config/PwmSettingTest.java
  42. 2 1
      server/src/test/java/password/pwm/config/stored/StoredConfigItemKeyTest.java
  43. 46 0
      server/src/test/java/password/pwm/health/HealthStatusTest.java
  44. 7 5
      webapp/src/main/webapp/public/reference/settings.jsp

+ 1 - 0
pom.xml

@@ -205,6 +205,7 @@
                         </goals>
                         <configuration>
                             <rules>
+                                <banDuplicatePomDependencyVersions/>
                                 <requireJavaVersion>
                                     <version>[11,)</version>
                                 </requireJavaVersion>

+ 2 - 2
server/src/main/java/password/pwm/PwmApplication.java

@@ -306,7 +306,6 @@ public class PwmApplication
             LOGGER.error( () -> "error outputting log to debug: " + e.getMessage() );
         }
 
-
         if ( this.getConfig() != null )
         {
             final Map<AppProperty, String> nonDefaultProperties = getConfig().readAllNonDefaultAppProperties();
@@ -392,14 +391,15 @@ public class PwmApplication
 
         try
         {
+            final Instant itemStartTime = Instant.now();
             SettingDataMaker.generateSettingData( this.getConfig().getStoredConfiguration(), null, PwmConstants.DEFAULT_LOCALE );
+            LOGGER.trace( () -> "completed setting data cache loading", () -> TimeDuration.fromCurrent( itemStartTime ) );
         }
         catch ( final Exception e )
         {
             LOGGER.debug( () -> "error initializing generateSettingData: " + e.getMessage() );
         }
 
-
         {
             final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( this, PwmApplication.class );
             pwmScheduler.scheduleDailyZuluZeroStartJob( new DailySummaryJob( this ), executorService, TimeDuration.ZERO );

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

@@ -94,6 +94,9 @@ public abstract class PwmConstants
     public static final String PROFILE_ID_ALL = "all";
     public static final String PROFILE_ID_DEFAULT = "default";
 
+    public static final String DOMAIN_ID_DEFAULT = "default";
+    public static final String DOMAIN_ID_PLACEHOLDER = "default";
+
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
 
     public static final String HTTP_BASIC_AUTH_PREFIX = readPwmConstantsBundle( "httpHeaderAuthorizationBasic" );

+ 48 - 29
server/src/main/java/password/pwm/bean/UserIdentity.java

@@ -25,6 +25,7 @@ import com.novell.ldapchai.exception.ChaiException;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.jetbrains.annotations.NotNull;
 import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.ErrorInformation;
@@ -39,6 +40,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 
 import java.io.Serializable;
+import java.util.Comparator;
 import java.util.Objects;
 import java.util.StringTokenizer;
 
@@ -55,29 +57,42 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
 
     private final String userDN;
     private final String ldapProfile;
+    private final String domain;
 
-    public UserIdentity( final String userDN, final String ldapProfile )
+    public enum Flag
+    {
+        PreCanonicalized,
+    }
+
+    private UserIdentity( final String userDN, final String ldapProfile, final String domain )
     {
         this.userDN = JavaHelper.requireNonEmpty( userDN, "UserIdentity: userDN value cannot be empty" );
         this.ldapProfile = JavaHelper.requireNonEmpty( ldapProfile, "UserIdentity: ldapProfile value cannot be empty" );
+        this.domain = JavaHelper.requireNonEmpty( domain, "UserIdentity: domain value cannot be empty" );
     }
 
-    public UserIdentity( final String userDN, final String ldapProfile, final boolean canonical )
+    public UserIdentity( final String userDN, final String ldapProfile, final String domain, final boolean canonical )
     {
-        if ( userDN == null || userDN.length() < 1 )
-        {
-            throw new IllegalArgumentException( "UserIdentity: userDN value cannot be empty" );
-        }
-        this.userDN = userDN;
-        this.ldapProfile = ldapProfile == null ? "" : ldapProfile;
+        this( userDN, ldapProfile, domain );
         this.canonical = canonical;
     }
 
+    public static UserIdentity createUserIdentity( final String userDN, final String ldapProfile, final Flag... flags )
+    {
+        final boolean canonical = JavaHelper.enumArrayContainsValue( flags, Flag.PreCanonicalized );
+        return new UserIdentity( userDN, ldapProfile, PwmConstants.DOMAIN_ID_DEFAULT, canonical );
+    }
+
     public String getUserDN( )
     {
         return userDN;
     }
 
+    public String getDomain()
+    {
+        return domain;
+    }
+
     public String getLdapProfileID( )
     {
         return ldapProfile;
@@ -183,7 +198,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         }
         final String profileID = st.nextToken();
         final String userDN = st.nextToken();
-        return new UserIdentity( userDN, profileID );
+        return createUserIdentity( userDN, profileID );
     }
 
     public static UserIdentity fromKey( final String key, final PwmApplication pwmApplication )
@@ -223,40 +238,44 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         {
             return false;
         }
-
         final UserIdentity that = ( UserIdentity ) o;
-
-        if ( !ldapProfile.equals( that.ldapProfile ) )
-        {
-            return false;
-        }
-        if ( !userDN.equals( that.userDN ) )
-        {
-            return false;
-        }
-
-        return true;
+        return Objects.equals( domain, that.domain )
+                && Objects.equals( ldapProfile, that.ldapProfile )
+                && Objects.equals( userDN, that.userDN );
     }
 
     @Override
-    public int hashCode( )
+    public int hashCode()
     {
-        int result = userDN.hashCode();
-        result = 31 * result + ldapProfile.hashCode();
-        return result;
+        return Objects.hash( domain, ldapProfile, userDN );
     }
 
     @Override
     public int compareTo( @NotNull final UserIdentity otherIdentity )
     {
-        return compareString().compareToIgnoreCase( otherIdentity.compareString() );
+        return comparator().compare( this, otherIdentity );
     }
 
-    private String compareString()
+    private static Comparator<UserIdentity> comparator( )
     {
-        return ( ldapProfile == null ? "_" : ldapProfile ) + "_" + userDN;
+        final Comparator<UserIdentity> domainComparator = Comparator.comparing(
+                UserIdentity::getDomain,
+                Comparator.nullsLast( Comparator.naturalOrder() ) );
+
+        final Comparator<UserIdentity> profileComparator = Comparator.comparing(
+                UserIdentity::getLdapProfileID,
+                Comparator.nullsLast( Comparator.naturalOrder() ) );
+
+        final Comparator<UserIdentity> userComparator = Comparator.comparing(
+                UserIdentity::getDomain,
+                Comparator.nullsLast( Comparator.naturalOrder() ) );
+
+        return domainComparator
+                .thenComparing( profileComparator )
+                .thenComparing( userComparator );
     }
 
+
     public UserIdentity canonicalized( final PwmApplication pwmApplication )
             throws PwmUnrecoverableException
     {
@@ -275,7 +294,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         {
             throw PwmUnrecoverableException.fromChaiException( e );
         }
-        final UserIdentity canonicalziedIdentity = new UserIdentity( userDN, this.getLdapProfileID() );
+        final UserIdentity canonicalziedIdentity = createUserIdentity( userDN, this.getLdapProfileID() );
         canonicalziedIdentity.canonical = true;
         return canonicalziedIdentity;
     }

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

@@ -104,7 +104,7 @@ public class Configuration
     public Configuration( final StoredConfiguration storedConfiguration )
     {
         this.storedConfiguration = storedConfiguration;
-        this.settingReader = new SettingReader( storedConfiguration, null );
+        this.settingReader = new SettingReader( storedConfiguration, null, PwmConstants.DOMAIN_ID_PLACEHOLDER );
     }
 
     public static void deprecatedSettingException( final PwmSetting pwmSetting, final String profile, final MessageSendMethod value )

+ 17 - 60
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -21,10 +21,10 @@
 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.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -36,7 +36,6 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -1284,18 +1283,22 @@ public enum PwmSetting
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),;
 
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSetting.class );
-
     private static final Map<String, PwmSetting> KEY_MAP = Collections.unmodifiableMap( Arrays.stream( values() )
             .collect( Collectors.toMap( PwmSetting::getKey, pwmSetting -> pwmSetting ) ) );
 
+    private static final Comparator<PwmSetting> MENU_LOCATION_COMPARATOR = Comparator.comparing(
+            pwmSetting -> pwmSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ),
+            Comparator.nullsLast( Comparator.naturalOrder() ) );
+
+    private static final List<PwmSetting> SORTED_VALUES = Collections.unmodifiableList( Arrays.stream( values() )
+            .sorted( MENU_LOCATION_COMPARATOR )
+            .collect( Collectors.toList() ) );
+
     private final String key;
     private final PwmSettingSyntax syntax;
     private final PwmSettingCategory category;
     private final PwmSettingMetaDataReader pwmSettingMetaDataReader;
 
-
-
     PwmSetting(
             final String key,
             final PwmSettingSyntax syntax,
@@ -1308,6 +1311,11 @@ public enum PwmSetting
         this.pwmSettingMetaDataReader = new PwmSettingMetaDataReader( this );
     }
 
+    public static Comparator<PwmSetting> menuLocationComparator()
+    {
+        return MENU_LOCATION_COMPARATOR;
+    }
+
     public String getKey( )
     {
         return key;
@@ -1385,7 +1393,7 @@ public enum PwmSetting
     public boolean isRequired( )
     {
         return pwmSettingMetaDataReader.isRequired();
-     }
+    }
 
     public boolean isHidden( )
     {
@@ -1420,43 +1428,9 @@ public enum PwmSetting
         return pwmSettingMetaDataReader.getLDAPPermissionInfo();
     }
 
-    public enum SettingStat
+    public static List<PwmSetting> sortedValues()
     {
-        Total,
-        hasProfile,
-        syntaxCounts,
-    }
-
-    public static Map<SettingStat, Object> getStats( )
-    {
-        final Map<SettingStat, Object> returnObj = new LinkedHashMap<>();
-        {
-            returnObj.put( SettingStat.Total, password.pwm.config.PwmSetting.values().length );
-        }
-        {
-            int hasProfile = 0;
-            for ( final PwmSetting pwmSetting : values() )
-            {
-                if ( pwmSetting.getCategory().hasProfiles() )
-                {
-                    hasProfile++;
-                }
-            }
-            returnObj.put( SettingStat.hasProfile, hasProfile );
-        }
-        {
-            final Map<PwmSettingSyntax, Integer> syntaxCounts = new LinkedHashMap<>();
-            for ( final PwmSettingSyntax syntax : PwmSettingSyntax.values() )
-            {
-                syntaxCounts.put( syntax, 0 );
-            }
-            for ( final PwmSetting pwmSetting : values() )
-            {
-                syntaxCounts.put( pwmSetting.getSyntax(), syntaxCounts.get( pwmSetting.getSyntax() ) + 1 );
-            }
-            returnObj.put( SettingStat.syntaxCounts, syntaxCounts );
-        }
-        return returnObj;
+        return SORTED_VALUES;
     }
 
     @Value
@@ -1497,21 +1471,4 @@ public enum PwmSetting
             return templateSetReferences.iterator().next().getReference();
         }
     }
-
-    public static Set<PwmSetting> sortedByMenuLocation( final Locale locale )
-    {
-        final Set<PwmSetting> sortedSet = new TreeSet<>( menuLocationComparator( locale ) );
-        sortedSet.addAll( KEY_MAP.values() );
-        return Collections.unmodifiableSet( sortedSet );
-    }
-
-    public static Comparator<PwmSetting> menuLocationComparator( final Locale locale )
-    {
-        return ( o1, o2 ) ->
-        {
-            final String selfValue = o1.toMenuLocationDebug( null, locale );
-            final String otherValue = o2.toMenuLocationDebug( null, locale );
-            return selfValue.compareTo( otherValue );
-        };
-    }
 }

+ 96 - 102
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -23,24 +23,25 @@ package password.pwm.config;
 import password.pwm.PwmConstants;
 import password.pwm.i18n.Config;
 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.XmlElement;
 import password.pwm.util.macro.MacroRequest;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 public enum PwmSettingCategory
 {
-
     TEMPLATES( null ),
     NOTES( null ),
 
@@ -193,18 +194,24 @@ public enum PwmSettingCategory
 
     INTERNAL( SETTINGS ),;
 
+    private static final Comparator<PwmSettingCategory> MENU_LOCATION_COMPARATOR = Comparator.comparing(
+            ( pwmSettingCategory ) -> pwmSettingCategory.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
 
-    private static List<PwmSettingCategory> cachedSortedSettings;
+    private static final Supplier<List<PwmSettingCategory>> SORTED_VALUES = new LazySupplier<>( () -> Collections.unmodifiableList( Arrays.stream( values() )
+            .sorted( MENU_LOCATION_COMPARATOR )
+            .collect( Collectors.toList() ) ) );
 
     private final PwmSettingCategory parent;
 
-    private final Supplier<Optional<PwmSetting>> profileSetting = new LazySupplier<>( () -> XmlReader.readProfileSettingFromXml( this, true ) );
-    private final Supplier<Integer> level = new LazySupplier<>( () -> XmlReader.readLevel( this ) );
-    private final Supplier<Boolean> hidden = new LazySupplier<>( () -> XmlReader.readHidden( this ) );
-    private final Supplier<Boolean> isTopLevelProfile = new LazySupplier<>( () -> XmlReader.readIsTopLevelProfile( this ) );
-    private final Supplier<String> defaultLocaleLabel = new LazySupplier<>( () -> XmlReader.readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
-    private final Supplier<String> defaultLocaleDescription = new LazySupplier<>( () -> XmlReader.readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
-
+    private final transient Supplier<Optional<PwmSetting>> profileSetting = new LazySupplier<>( () -> DataReader.readProfileSettingFromXml( this, true ) );
+    private final transient Supplier<Integer> level = new LazySupplier<>( () -> DataReader.readLevel( this ) );
+    private final transient Supplier<Boolean> hidden = new LazySupplier<>( () -> DataReader.readHidden( this ) );
+    private final transient Supplier<Boolean> isTopLevelProfile = new LazySupplier<>( () -> DataReader.readIsTopLevelProfile( this ) );
+    private final transient Supplier<String> defaultLocaleLabel = new LazySupplier<>( () -> DataReader.readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
+    private final transient Supplier<String> defaultLocaleDescription = new LazySupplier<>( () -> DataReader.readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
+    private final transient Supplier<PwmSettingScope> scope = new LazySupplier<>( () -> DataReader.readScope( this ) );
+    private final transient Supplier<Set<PwmSettingCategory>> children = new LazySupplier<>( () -> DataReader.readChildren( this ) );
+    private final transient Supplier<Set<PwmSetting>> settings = new LazySupplier<>( () -> DataReader.readSettings( this ) );
 
     PwmSettingCategory( final PwmSettingCategory parent )
     {
@@ -243,7 +250,7 @@ public enum PwmSettingCategory
             return defaultLocaleLabel.get();
         }
 
-        return XmlReader.readLabel( this, locale );
+        return DataReader.readLabel( this, locale );
     }
 
     public String getDescription( final Locale locale )
@@ -253,7 +260,7 @@ public enum PwmSettingCategory
             return defaultLocaleDescription.get();
         }
 
-        return XmlReader.readDescription( this, locale );
+        return DataReader.readDescription( this, locale );
     }
 
     public int getLevel( )
@@ -261,7 +268,6 @@ public enum PwmSettingCategory
         return level.get();
     }
 
-
     public boolean isHidden( )
     {
         return hidden.get();
@@ -272,42 +278,24 @@ public enum PwmSettingCategory
         return getParent() == null;
     }
 
-    public Collection<PwmSettingCategory> getParents( )
+    public PwmSettingScope getScope()
     {
-        final ArrayList<PwmSettingCategory> returnObj = new ArrayList<>();
-        PwmSettingCategory currentCategory = this.getParent();
-        while ( currentCategory != null )
-        {
-            returnObj.add( 0, currentCategory );
-            currentCategory = currentCategory.getParent();
-        }
-        return returnObj;
+        return scope.get();
     }
 
-    public Collection<PwmSettingCategory> getChildCategories( )
+    public boolean hasChildren()
     {
-        final ArrayList<PwmSettingCategory> returnObj = new ArrayList<>();
-        for ( final PwmSettingCategory category : values() )
-        {
-            if ( this == category.getParent() )
-            {
-                returnObj.add( category );
-            }
-        }
-        return returnObj;
+        return !getChildren().isEmpty();
     }
 
-    public List<PwmSetting> getSettings( )
+    public Set<PwmSettingCategory> getChildren( )
     {
-        final List<password.pwm.config.PwmSetting> returnList = new ArrayList<>();
-        for ( final password.pwm.config.PwmSetting setting : password.pwm.config.PwmSetting.values() )
-        {
-            if ( setting.getCategory() == this )
-            {
-                returnList.add( setting );
-            }
-        }
-        return Collections.unmodifiableList( returnList );
+        return children.get();
+    }
+
+    public Set<PwmSetting> getSettings( )
+    {
+        return settings.get();
     }
 
     public String toMenuLocationDebug(
@@ -355,72 +343,51 @@ public enum PwmSettingCategory
         return sb.toString();
     }
 
-    public static List<PwmSettingCategory> sortedValues( final Locale locale )
+    public static List<PwmSettingCategory> sortedValues( )
     {
-        if ( cachedSortedSettings == null )
-        {
-            final List<PwmSettingCategory> tempList = new ArrayList<>( Arrays.asList( PwmSettingCategory.values() ) );
-            tempList.sort( menuLocationComparator( locale ) );
-            cachedSortedSettings = Collections.unmodifiableList( tempList );
-        }
-        return cachedSortedSettings;
+        return SORTED_VALUES.get();
     }
 
-    private static Comparator<PwmSettingCategory> menuLocationComparator( final Locale locale )
+    public static List<PwmSettingCategory> valuesForReferenceDoc()
     {
-        return ( o1, o2 ) ->
-        {
-            final String selfValue = o1.toMenuLocationDebug( null, locale );
-            final String otherValue = o2.toMenuLocationDebug( null, locale );
-            return selfValue.compareTo( otherValue );
-        };
+        final List<PwmSettingCategory> values = sortedValues().stream()
+                .filter( ( category ) -> !category.isHidden() )
+                .filter( ( category ) -> !category.getSettings().isEmpty() )
+                .collect( Collectors.toList( ) );
+
+        return Collections.unmodifiableList( values );
     }
 
-    public static List<PwmSettingCategory> valuesForReferenceDoc( final Locale locale )
+    public static List<PwmSettingCategory> associatedProfileCategories( final PwmSettingCategory inputCategory )
     {
-        final List<PwmSettingCategory> values = new ArrayList<>( sortedValues( locale ) );
-        for ( final Iterator<PwmSettingCategory> iterator = values.iterator(); iterator.hasNext(); )
+        if ( inputCategory == null || !inputCategory.hasProfiles() )
         {
-            final PwmSettingCategory category = iterator.next();
-            if ( category.isHidden() )
-            {
-                iterator.remove();
-            }
-            else if ( category.getSettings().isEmpty() )
-            {
-                iterator.remove();
-            }
+            return Collections.emptyList();
         }
-        return Collections.unmodifiableList( values );
-    }
 
-    public static Collection<PwmSettingCategory> associatedProfileCategories( final PwmSettingCategory inputCategory )
-    {
-        final Collection<PwmSettingCategory> returnValues = new ArrayList<>();
-        if ( inputCategory != null && inputCategory.hasProfiles() )
+        final List<PwmSettingCategory> returnValues = new ArrayList<>();
+
+        PwmSettingCategory topLevelCategory = inputCategory;
+        while ( !topLevelCategory.isTopLevelProfile() )
         {
-            PwmSettingCategory topLevelCategory = inputCategory;
-            while ( !topLevelCategory.isTopLevelProfile() )
-            {
-                topLevelCategory = topLevelCategory.getParent();
-            }
-            returnValues.add( topLevelCategory );
-            returnValues.addAll( topLevelCategory.getChildCategories() );
+            topLevelCategory = topLevelCategory.getParent();
         }
+        returnValues.add( topLevelCategory );
+        returnValues.addAll( topLevelCategory.getChildren() );
 
-        return Collections.unmodifiableCollection( returnValues );
+        return Collections.unmodifiableList( returnValues );
     }
 
     public static Optional<PwmSettingCategory> forKey( final String key )
     {
-        return Arrays.stream( values() )
+        return sortedValues().stream()
                 .filter( loopValue -> loopValue.getKey().equals( key ) )
                 .findFirst();
     }
 
     public static Optional<PwmSettingCategory> forProfileSetting( final PwmSetting setting )
     {
-        for ( final PwmSettingCategory loopCategory : PwmSettingCategory.values() )
+        for ( final PwmSettingCategory loopCategory : sortedValues() )
         {
             if ( loopCategory.hasProfiles() )
             {
@@ -435,9 +402,8 @@ public enum PwmSettingCategory
         return Optional.empty();
     }
 
-    private static class XmlReader
+    private static class DataReader
     {
-
         private static Optional<PwmSetting> readProfileSettingFromXml( final PwmSettingCategory category, final boolean nested )
         {
             PwmSettingCategory nextCategory = category;
@@ -469,29 +435,41 @@ public enum PwmSettingCategory
         private static int readLevel( final PwmSettingCategory category )
         {
             final XmlElement settingElement = PwmSettingXml.readCategoryXml( category );
-            final String levelAttribute = settingElement.getAttributeValue( "level" );
+            final String levelAttribute = settingElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_LEVEL );
             return levelAttribute != null ? Integer.parseInt( levelAttribute ) : 0;
         }
 
+        private static PwmSettingScope readScope( final PwmSettingCategory category )
+        {
+            final String attributeValue = readAttributeFromCategoryOrParent( category, PwmSettingXml.XML_ELEMENT_SCOPE );
+            return JavaHelper.readEnumFromString( PwmSettingScope.class, attributeValue ).orElseThrow( () -> new IllegalStateException(
+                    "unable to parse value for PwmSettingCategory '" + category + "' scope attribute" ) );
+        }
+
         private static boolean readHidden( final PwmSettingCategory category )
         {
-            final XmlElement settingElement = PwmSettingXml.readCategoryXml( category );
-            final String hiddenElement = settingElement.getAttributeValue( "hidden" );
-            if ( hiddenElement != null && "true".equalsIgnoreCase( hiddenElement ) )
-            {
-                return true;
-            }
-            else
+            final String attributeValue = readAttributeFromCategoryOrParent( category, PwmSettingXml.XML_ELEMENT_HIDDEN );
+            return "true".equalsIgnoreCase( attributeValue );
+        }
+
+        private static String readAttributeFromCategoryOrParent(
+                final PwmSettingCategory category,
+                final String attribute
+        )
+        {
+            PwmSettingCategory nextCategory = category;
+            while ( nextCategory != null )
             {
-                for ( final PwmSettingCategory parentCategory : category.getParents() )
+                final XmlElement settingElement = PwmSettingXml.readCategoryXml( category );
+                final String attributeValue = settingElement.getAttributeValue( attribute );
+                if ( !StringUtil.isEmpty( attributeValue ) )
                 {
-                    if ( parentCategory.isHidden() )
-                    {
-                        return true;
-                    }
+                    return attributeValue;
                 }
+                nextCategory = nextCategory.getParent();
             }
-            return false;
+            throw new IllegalStateException( "can't read attribute '"
+                    + attribute + "' from category '" + category + "' or ancestor." );
         }
 
         private static boolean readIsTopLevelProfile( final PwmSettingCategory category )
@@ -515,5 +493,21 @@ public enum PwmSettingCategory
             final MacroRequest macroRequest = MacroRequest.forStatic();
             return macroRequest.expandMacros( storedText );
         }
+
+        public static Set<PwmSettingCategory> readChildren( final PwmSettingCategory category )
+        {
+            final Set<PwmSettingCategory> categories = Arrays.stream( PwmSettingCategory.values() )
+                    .filter( ( loopCategory ) -> loopCategory.getParent() == category )
+                    .collect( Collectors.toSet() );
+            return Collections.unmodifiableSet( JavaHelper.copiedEnumSet( categories, PwmSettingCategory.class ) );
+        }
+
+        public static Set<PwmSetting> readSettings( final PwmSettingCategory category )
+        {
+            final Set<PwmSetting> settings = Arrays.stream( PwmSetting.values() )
+                    .filter( ( setting ) -> setting.getCategory() == category )
+                    .collect( Collectors.toSet() );
+            return Collections.unmodifiableSet( JavaHelper.copiedEnumSet( settings, PwmSetting.class ) );
+        }
     }
 }

+ 27 - 0
server/src/main/java/password/pwm/config/PwmSettingScope.java

@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+public enum PwmSettingScope
+{
+    SYSTEM,
+    DOMAIN,
+}

+ 66 - 0
server/src/main/java/password/pwm/config/PwmSettingStats.java

@@ -0,0 +1,66 @@
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.Map;
+
+public class PwmSettingStats
+{
+    public enum SettingStat
+    {
+        Total,
+        hasProfile,
+        syntaxCounts,
+    }
+
+    public static Map<SettingStat, Object> getStats( )
+    {
+        final Map<SettingStat, Object> returnObj = new LinkedHashMap<>();
+        {
+            returnObj.put( SettingStat.Total, PwmSetting.values().length );
+        }
+        {
+            int hasProfile = 0;
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                if ( pwmSetting.getCategory().hasProfiles() )
+                {
+                    hasProfile++;
+                }
+            }
+            returnObj.put( SettingStat.hasProfile, hasProfile );
+        }
+        {
+            final Map<PwmSettingSyntax, Integer> syntaxCounts = new LinkedHashMap<>();
+            for ( final PwmSettingSyntax syntax : PwmSettingSyntax.values() )
+            {
+                syntaxCounts.put( syntax, 0 );
+            }
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                syntaxCounts.put( pwmSetting.getSyntax(), syntaxCounts.get( pwmSetting.getSyntax() ) + 1 );
+            }
+            returnObj.put( SettingStat.syntaxCounts, syntaxCounts );
+        }
+        return returnObj;
+    }
+}

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

@@ -64,11 +64,13 @@ public class PwmSettingXml
     static final String XML_ELEMENT_VALUE = "value";
     static final String XML_ELEMENT_OPTION = "option";
     static final String XML_ELEMENT_OPTIONS = "options";
+    static final String XML_ELEMENT_SCOPE = "scope";
+
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
-    private static final LazySoftReference<XmlDocument> XML_DOC_CACHE = new LazySoftReference<>( () -> readXml() );
+    private static final LazySoftReference<XmlDocument> XML_DOC_CACHE = new LazySoftReference<>( PwmSettingXml::readXml );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
 
     private static XmlDocument readXml( )

+ 6 - 3
server/src/main/java/password/pwm/config/SettingReader.java

@@ -35,17 +35,20 @@ import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 public class SettingReader
 {
     private final StoredConfiguration storedConfiguration;
     private final String profileID;
+    private final String domainID;
 
-    public SettingReader( final StoredConfiguration storedConfiguration, final String profileID )
+    public SettingReader( final StoredConfiguration storedConfiguration, final String profileID, final String domainID )
     {
-        this.storedConfiguration = storedConfiguration;
-        this.profileID = profileID;
+        this.storedConfiguration = Objects.requireNonNull( storedConfiguration );
+        this.profileID = Objects.requireNonNull( profileID );
+        this.domainID = Objects.requireNonNull( domainID );
     }
 
     public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )

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

@@ -20,6 +20,7 @@
 
 package password.pwm.config.profile;
 
+import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingReader;
 import password.pwm.config.option.IdentityVerificationMethod;
@@ -49,7 +50,7 @@ public abstract class AbstractProfile implements Profile
     {
         this.identifier = identifier;
         this.storedConfiguration = storedConfiguration;
-        this.settingReader = new SettingReader( storedConfiguration, identifier );
+        this.settingReader = new SettingReader( storedConfiguration, identifier, PwmConstants.DOMAIN_ID_PLACEHOLDER );
     }
 
     @Override

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

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

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

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

+ 22 - 16
server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java

@@ -37,7 +37,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-public class StoredConfigItemKey implements Serializable, Comparable<StoredConfigItemKey>
+public class StoredConfigItemKey implements Serializable
 {
     public enum RecordType
     {
@@ -59,19 +59,19 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
     }
 
     private final RecordType recordType;
+    private final String domainID;
     private final String recordID;
     private final String profileID;
 
     private static final long serialVersionUID = 1L;
 
-
-
-    private StoredConfigItemKey( final RecordType recordType, final String recordID, final String profileID )
+    private StoredConfigItemKey( final RecordType recordType, final String domainID, final String recordID, final String profileID )
     {
         Objects.requireNonNull( recordType, "recordType can not be null" );
         Objects.requireNonNull( recordID, "recordID can not be null" );
 
         this.recordType = recordType;
+        this.domainID = domainID;
         this.recordID = recordID;
         this.profileID = profileID;
     }
@@ -81,6 +81,11 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         return recordType;
     }
 
+    public String getDomainID()
+    {
+        return domainID;
+    }
+
     public String getRecordID()
     {
         return recordID;
@@ -93,17 +98,17 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
 
     static StoredConfigItemKey fromSetting( final PwmSetting pwmSetting, final String profileID )
     {
-        return new StoredConfigItemKey( RecordType.SETTING, pwmSetting.getKey(), profileID );
+        return new StoredConfigItemKey( RecordType.SETTING, null, pwmSetting.getKey(), profileID );
     }
 
     static StoredConfigItemKey fromLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
     {
-        return new StoredConfigItemKey( RecordType.LOCALE_BUNDLE, localeBundle.getKey(), key );
+        return new StoredConfigItemKey( RecordType.LOCALE_BUNDLE, null, localeBundle.getKey(), key );
     }
 
     static StoredConfigItemKey fromConfigurationProperty( final ConfigurationProperty configurationProperty )
     {
-        return new StoredConfigItemKey( RecordType.PROPERTY, configurationProperty.getKey(), null );
+        return new StoredConfigItemKey( RecordType.PROPERTY, null, configurationProperty.getKey(), null );
     }
 
     public boolean isRecordType( final RecordType recordType )
@@ -234,6 +239,7 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         }
         final StoredConfigItemKey that = ( StoredConfigItemKey ) o;
         return Objects.equals( recordType, that.recordType )
+                && Objects.equals( domainID, that.domainID )
                 && Objects.equals( recordID, that.recordID )
                 && Objects.equals( profileID, that.profileID );
     }
@@ -250,12 +256,6 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         return getLabel( PwmConstants.DEFAULT_LOCALE );
     }
 
-    @Override
-    public int compareTo( final StoredConfigItemKey o )
-    {
-        return comparator( PwmConstants.DEFAULT_LOCALE ).compare( this, o );
-    }
-
     public PwmSettingSyntax getSyntax()
     {
         switch ( getRecordType() )
@@ -293,18 +293,21 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         return Collections.unmodifiableSet( input.stream().filter( ( k ) -> k.isRecordType( recordType ) ).collect( Collectors.toSet() ) );
     }
 
-    private static Comparator<StoredConfigItemKey> comparator( final Locale locale )
+    public static Comparator<StoredConfigItemKey> comparator( final Locale locale )
     {
         final Comparator<StoredConfigItemKey> typeComparator = Comparator.comparing(
                 StoredConfigItemKey::getRecordType,
                 Comparator.nullsLast( Comparator.naturalOrder() ) );
 
+        final Comparator<StoredConfigItemKey> domainComparator = Comparator.comparing( StoredConfigItemKey::getDomainID,
+                Comparator.nullsLast( Comparator.naturalOrder() ) );
+
         final Comparator<StoredConfigItemKey> recordComparator = ( o1, o2 ) ->
         {
             if ( Objects.equals( o1.getRecordType(), o2.getRecordType() )
                     && o1.isRecordType( RecordType.SETTING ) )
             {
-                final Comparator<PwmSetting> pwmSettingComparator = PwmSetting.menuLocationComparator( locale );
+                final Comparator<PwmSetting> pwmSettingComparator = PwmSetting.menuLocationComparator( );
                 return pwmSettingComparator.compare( o1.toPwmSetting(), o2.toPwmSetting() );
             }
             else
@@ -316,7 +319,10 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         final Comparator<StoredConfigItemKey> profileComparator = Comparator.comparing( StoredConfigItemKey::getProfileID,
                 Comparator.nullsLast( Comparator.naturalOrder() ) );
 
-        return typeComparator.thenComparing( recordComparator ).thenComparing( profileComparator );
+        return typeComparator
+                .thenComparing( domainComparator )
+                .thenComparing( recordComparator )
+                .thenComparing( profileComparator );
     }
 
 

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

@@ -426,7 +426,7 @@ public abstract class StoredConfigurationUtil
             }
         };
 
-        return Collections.unmodifiableSet( Stream.of( PwmSetting.values() )
+        return Collections.unmodifiableSet( PwmSetting.sortedValues().stream()
                 .parallel()
                 .flatMap( function )
                 .collect( Collectors.collectingAndThen( Collectors.toSet(), Collections::unmodifiableSet ) ) );

+ 15 - 6
server/src/main/java/password/pwm/config/value/data/UserPermission.java

@@ -27,6 +27,7 @@ import password.pwm.ldap.permission.UserPermissionType;
 import password.pwm.util.java.StringUtil;
 
 import java.io.Serializable;
+import java.util.Comparator;
 
 @Value
 @Builder
@@ -42,6 +43,19 @@ public class UserPermission implements Serializable, Comparable<UserPermission>
     private String ldapQuery;
     private String ldapBase;
 
+    private static final Comparator<UserPermission> COMPARATOR = Comparator.comparing(
+            UserPermission::getType,
+            Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    UserPermission::getLdapProfileID,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    UserPermission::getLdapBase,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    UserPermission::getLdapQuery,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) );
+
     public UserPermissionType getType( )
     {
         return type == null ? UserPermissionType.ldapQuery : type;
@@ -60,11 +74,6 @@ public class UserPermission implements Serializable, Comparable<UserPermission>
     @Override
     public int compareTo( @NotNull final UserPermission o )
     {
-        return makeComparisonString().compareTo( o.makeComparisonString() );
-    }
-
-    private String makeComparisonString()
-    {
-        return getType().ordinal() + "-" + getLdapProfileID() + "-" + getLdapBase() + "-" + getLdapQuery();
+        return COMPARATOR.compare( this, o );
     }
 }

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

@@ -131,7 +131,10 @@ public enum HealthMessage
         return HealthMessage.class.getSimpleName() + "_" + this.toString();
     }
 
-    public String getDescription( final Locale locale, final password.pwm.config.Configuration config, final String[] fields )
+    public String getDescription(
+            final Locale locale,
+            final password.pwm.config.Configuration config,
+            final String[] fields )
     {
         return LocaleHelper.getLocalizedMessage( locale, this.getKey(), config, Health.class, fields );
     }

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

@@ -48,6 +48,7 @@ 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;
@@ -150,18 +151,15 @@ public class HealthMonitor implements PwmService
 
     public static HealthStatus getMostSevereHealthStatus( final Collection<HealthRecord> healthRecords )
     {
-        HealthStatus returnStatus = HealthStatus.GOOD;
+        final EnumSet<HealthStatus> tempSet = EnumSet.noneOf( HealthStatus.class );
         if ( healthRecords != null )
         {
             for ( final HealthRecord record : healthRecords )
             {
-                if ( record.getStatus().getSeverityLevel() > returnStatus.getSeverityLevel() )
-                {
-                    returnStatus = record.getStatus();
-                }
+                tempSet.add( record.getStatus() );
             }
         }
-        return returnStatus;
+        return HealthStatus.mostSevere( tempSet ).orElse( HealthStatus.GOOD );
     }
 
     @Override

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

@@ -26,9 +26,13 @@ import password.pwm.ws.server.rest.bean.HealthData;
 
 import java.io.Serializable;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 @EqualsAndHashCode
 public class HealthRecord implements Serializable, Comparable<HealthRecord>
@@ -38,23 +42,31 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
     // new fields
     private final HealthTopic topic;
     private final HealthMessage message;
-    private final String[] fields;
+    private final List<String> fields;
 
     // old fields
     private final String oldTopic;
     private final String oldDetail;
 
+    private static final Comparator<HealthRecord> COMPARATOR = Comparator.comparing(
+            HealthRecord::getStatus,
+            Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    healthRecord -> healthRecord.getTopic( null, null ),
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    healthRecord -> healthRecord.getDetail( null, null ),
+                    Comparator.nullsLast( Comparator.naturalOrder() ) );
+
+
+    @Deprecated
     public HealthRecord(
             final HealthStatus status,
             final String topic,
             final String detail
     )
     {
-        if ( status == null )
-        {
-            throw new NullPointerException( "status cannot be null" );
-        }
-        this.status = status;
+        this.status = Objects.requireNonNull( status,  "status cannot be null" );
 
         this.oldTopic = topic;
         this.oldDetail = detail;
@@ -64,18 +76,14 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         this.fields = null;
     }
 
+    @Deprecated
     public HealthRecord(
             final HealthStatus status,
             final HealthTopic topic,
             final String detail
     )
     {
-        if ( status == null )
-        {
-            throw new NullPointerException( "status cannot be null" );
-        }
-        this.status = status;
-
+        this.status = Objects.requireNonNull( status,  "status cannot be null" );
         this.oldTopic = null;
         this.oldDetail = detail;
 
@@ -88,19 +96,13 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
             final HealthStatus status,
             final HealthTopic topicEnum,
             final HealthMessage message,
-            final String[] fields
+            final List<String> fields
     )
     {
-
-        if ( status == null )
-        {
-            throw new NullPointerException( "status cannot be null" );
-        }
-        this.status = status;
-
-        this.topic = topicEnum;
-        this.message = message;
-        this.fields = fields;
+        this.status = Objects.requireNonNull( status,  "status cannot be null" );
+        this.topic = Objects.requireNonNull( topicEnum,  "topic cannot be null" );
+        this.message = Objects.requireNonNull( message,  "message cannot be null" );
+        this.fields = fields == null ? Collections.emptyList() : Collections.unmodifiableList( new ArrayList<>( fields ) );
 
         this.oldTopic = null;
         this.oldDetail = null;
@@ -108,15 +110,15 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
 
     public static HealthRecord forMessage( final HealthMessage message )
     {
-        return new HealthRecord( message.getStatus(), message.getTopic(), message, null );
+        return new HealthRecord( message.getStatus(), message.getTopic(), message, Collections.emptyList() );
     }
 
     public static HealthRecord forMessage( final HealthMessage message, final String... fields )
     {
-        return new HealthRecord( message.getStatus(), message.getTopic(), message, fields );
+        final List<String> fieldList = fields == null ? Collections.emptyList() : Arrays.asList( fields );
+        return new HealthRecord( message.getStatus(), message.getTopic(), message, fieldList );
     }
 
-
     public HealthStatus getStatus( )
     {
         return status;
@@ -128,7 +130,11 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         {
             return oldTopic;
         }
-        return this.topic.getDescription( locale, config );
+        if ( topic != null )
+        {
+            return this.topic.getDescription( locale, config );
+        }
+        return "";
     }
 
     public String getDetail( final Locale locale, final Configuration config )
@@ -137,7 +143,11 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         {
             return oldDetail;
         }
-        return this.message.getDescription( locale, config, fields );
+        if ( message != null )
+        {
+            return this.message.getDescription( locale, config, fields.toArray( new String[0] ) );
+        }
+        return "";
     }
 
     public String toDebugString( final Locale locale, final Configuration config )
@@ -149,28 +159,9 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
     @Override
     public int compareTo( final HealthRecord otherHealthRecord )
     {
-        final int statusCompare = status.compareTo( otherHealthRecord.status );
-        if ( statusCompare != 0 )
-        {
-            return statusCompare;
-        }
-
-        final int topicCompare = this.getTopic( null, null ).compareTo( otherHealthRecord.getTopic( null, null ) );
-        if ( topicCompare != 0 )
-        {
-            return topicCompare;
-        }
-
-        final int detailCompare = this.getDetail( null, null ).compareTo( otherHealthRecord.getDetail( null, null ) );
-        if ( detailCompare != 0 )
-        {
-            return detailCompare;
-        }
-
-        return 0;
+        return COMPARATOR.compare( this, otherHealthRecord );
     }
 
-
     public List<HealthRecord> singletonList( )
     {
         return Collections.singletonList( this );

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

@@ -22,24 +22,21 @@ package password.pwm.health;
 
 import password.pwm.i18n.Health;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
 
+import java.util.Collection;
+import java.util.EnumSet;
 import java.util.Locale;
+import java.util.Optional;
 
 public enum HealthStatus
 {
-    WARN( 4 ),
-    CAUTION( 3 ),
-    CONFIG( 2 ),
-    GOOD( 1 ),
-    INFO( 0 ),
-    DEBUG( -1 ),;
-
-    private int severityLevel;
-
-    HealthStatus( final int severityLevel )
-    {
-        this.severityLevel = severityLevel;
-    }
+    WARN,
+    CAUTION,
+    CONFIG,
+    GOOD,
+    INFO,
+    DEBUG,;
 
     public String getKey( )
     {
@@ -51,8 +48,14 @@ public enum HealthStatus
         return LocaleHelper.getLocalizedMessage( locale, this.getKey(), config, Health.class );
     }
 
-    public int getSeverityLevel( )
+    public static Optional<HealthStatus> mostSevere( final Collection<HealthStatus> healthStatuses )
     {
-        return severityLevel;
+        // enumset will sort in natural order, with most severe first.
+        final EnumSet<HealthStatus> sortedSet = JavaHelper.copiedEnumSet( healthStatuses, HealthStatus.class );
+        if ( sortedSet.isEmpty() )
+        {
+            return Optional.empty();
+        }
+        return Optional.of( sortedSet.iterator().next() );
     }
 }

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

@@ -289,7 +289,7 @@ public class LDAPHealthChecker implements HealthChecker
                 else
                 {
                     final Locale locale = PwmConstants.DEFAULT_LOCALE;
-                    final UserIdentity userIdentity = new UserIdentity( testUserDN, ldapProfile.getIdentifier() );
+                    final UserIdentity userIdentity = UserIdentity.createUserIdentity( testUserDN, ldapProfile.getIdentifier() );
 
                     final PwmPasswordPolicy passwordPolicy = PasswordUtility.readPasswordPolicyForUser(
                             pwmApplication, null, userIdentity, theUser, locale );
@@ -365,7 +365,7 @@ public class LDAPHealthChecker implements HealthChecker
 
             try
             {
-                final UserIdentity userIdentity = new UserIdentity( theUser.getEntryDN(), ldapProfile.getIdentifier() );
+                final UserIdentity userIdentity = UserIdentity.createUserIdentity( theUser.getEntryDN(), ldapProfile.getIdentifier() );
                 final UserInfo userInfo = UserInfoFactory.newUserInfo(
                         pwmApplication,
                         SessionLabel.HEALTH_SESSION_LABEL,
@@ -933,7 +933,7 @@ public class LDAPHealthChecker implements HealthChecker
                     }
                 }
 
-                final UserIdentity newUserTemplateIdentity = new UserIdentity( policyUserStr, ldapProfile.getIdentifier() );
+                final UserIdentity newUserTemplateIdentity = UserIdentity.createUserIdentity( policyUserStr, ldapProfile.getIdentifier() );
 
                 final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser( newUserTemplateIdentity );
 

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

@@ -32,7 +32,6 @@ 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 javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
@@ -52,8 +51,6 @@ import java.util.Set;
 
 public class PwmHttpRequestWrapper
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmHttpRequestWrapper.class );
-
     private final HttpServletRequest httpServletRequest;
     private final Configuration configuration;
 
@@ -306,7 +303,7 @@ public class PwmHttpRequestWrapper
             }
         }
 
-        return resultSet;
+        return Collections.unmodifiableList( resultSet );
     }
 
     public String readHeaderValueAsString( final HttpHeader headerName )
@@ -411,9 +408,7 @@ public class PwmHttpRequestWrapper
 
     private static String decodeStringToDefaultCharSet( final String input )
     {
-        String decodedValue = input;
-        decodedValue = new String( input.getBytes( StandardCharsets.ISO_8859_1 ), PwmConstants.DEFAULT_CHARSET );
-        return decodedValue;
+        return new String( input.getBytes( StandardCharsets.ISO_8859_1 ), PwmConstants.DEFAULT_CHARSET );
     }
 
     public HttpMethod getMethod( )

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

@@ -532,6 +532,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return null;
     }
 
+    @Override
     public String toString( )
     {
         return this.getClass().getSimpleName() + " "
@@ -611,4 +612,9 @@ public class PwmRequest extends PwmHttpRequestWrapper
     {
         return requestStartTime;
     }
+
+    public String getDomainID()
+    {
+        return PwmConstants.DOMAIN_ID_DEFAULT;
+    }
 }

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

@@ -465,7 +465,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet
             LOGGER.info( pwmRequest, () -> "created user object: " + guestUserDN );
 
             final ChaiUser theUser = provider.getEntryFactory().newChaiUser( guestUserDN );
-            final UserIdentity userIdentity = new UserIdentity( guestUserDN, pwmSession.getUserInfo().getUserIdentity().getLdapProfileID() );
+            final UserIdentity userIdentity = UserIdentity.createUserIdentity( guestUserDN, pwmSession.getUserInfo().getUserIdentity().getLdapProfileID() );
 
             // write the expiration date:
             if ( expirationDate != null )

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

@@ -286,7 +286,7 @@ public class ConfigGuideUtils
                     pwmRequest.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( new Configuration( storedConfiguration ) ) );
 
             final String adminDN = form.get( ConfigGuideFormField.PARAM_LDAP_ADMIN_USER );
-            final UserIdentity adminIdentity = new UserIdentity( adminDN, PwmConstants.PROFILE_ID_DEFAULT );
+            final UserIdentity adminIdentity = UserIdentity.createUserIdentity( adminDN, PwmConstants.PROFILE_ID_DEFAULT );
 
             final UserMatchViewerFunction userMatchViewerFunction = new UserMatchViewerFunction();
             final Collection<UserIdentity> results = userMatchViewerFunction.discoverMatchingUsers(

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

@@ -26,9 +26,9 @@ import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
-import password.pwm.config.value.StoredValue;
 import password.pwm.config.stored.StoredConfigItemKey;
 import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.PwmUnrecoverableException;
@@ -51,6 +51,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 
@@ -149,7 +150,7 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
             }
         }
 
-        Collections.sort( certificateDebugDataItems );
+        certificateDebugDataItems.sort( CertificateDebugDataItem.getExpirationComparator() );
         return Collections.unmodifiableList( certificateDebugDataItems );
     }
 
@@ -201,7 +202,7 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
 
     @Value
     @Builder
-    public static class CertificateDebugDataItem implements Serializable, Comparable<CertificateDebugDataItem>
+    public static class CertificateDebugDataItem implements Serializable
     {
         private String menuLocation;
         private String subject;
@@ -211,15 +212,14 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
         private Instant issueDate;
         private String detail;
 
-        @Override
-        public int compareTo( final CertificateDebugDataItem o )
-        {
-            if ( this == o || this.equals( o ) )
-            {
-                return 0;
-            }
+        private static final Comparator<CertificateDebugDataItem> EXPIRATION_COMPARATOR = Comparator.comparing(
+                CertificateDebugDataItem::getExpirationDate,
+                Comparator.nullsLast( Comparator.naturalOrder() )
+        );
 
-            return expirationDate.compareTo( o.getExpirationDate() );
+        public static Comparator<CertificateDebugDataItem> getExpirationComparator()
+        {
+            return EXPIRATION_COMPARATOR;
         }
     }
 }

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

@@ -308,7 +308,7 @@ class NewUserUtils
         remoteWriteFormData( pwmRequest, newUserForm );
 
         // authenticate the user to pwm
-        final UserIdentity userIdentity = new UserIdentity( newUserDN, newUserProfile.getLdapProfile().getIdentifier() );
+        final UserIdentity userIdentity = UserIdentity.createUserIdentity( newUserDN, newUserProfile.getLdapProfile().getIdentifier() );
         final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmRequest, PwmAuthenticationSource.NEW_USER_REGISTRATION );
         sessionAuthenticator.authenticateUser( userIdentity, userPassword );
 

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

@@ -431,7 +431,7 @@ class PeopleSearchDataReader
         final boolean checkUserDNValues = Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN ) );
         for ( final String userDN : ldapValues )
         {
-            final UserIdentity loopIdentity = new UserIdentity( userDN, userIdentity.getLdapProfileID() );
+            final UserIdentity loopIdentity = UserIdentity.createUserIdentity( userDN, userIdentity.getLdapProfileID() );
             if ( returnObj.size() < maxValues )
             {
                 if ( checkUserDNValues )

+ 1 - 1
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -169,7 +169,7 @@ public class UserSearchEngine implements PwmService
                     {
                         final String canonicalDN;
                         canonicalDN = theUser.readCanonicalDN();
-                        return new UserIdentity( canonicalDN, inputIdentity.getLdapProfileID() );
+                        return UserIdentity.createUserIdentity( canonicalDN, inputIdentity.getLdapProfileID() );
                     }
                 }
                 catch ( final ChaiOperationException e )

+ 4 - 1
server/src/main/java/password/pwm/ldap/search/UserSearchJob.java

@@ -128,7 +128,10 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
         {
             final String userDN = entry.getKey();
             final Map<String, String> attributeMap = entry.getValue();
-            final UserIdentity userIdentity = new UserIdentity( userDN, userSearchJobParameters.getLdapProfile().getIdentifier(), true );
+            final UserIdentity userIdentity = UserIdentity.createUserIdentity(
+                    userDN,
+                    userSearchJobParameters.getLdapProfile().getIdentifier(),
+                    UserIdentity.Flag.PreCanonicalized );
             returnMap.put( userIdentity, attributeMap );
         }
         return returnMap;

+ 2 - 2
server/src/main/java/password/pwm/svc/event/DatabaseUserHistory.java

@@ -62,11 +62,11 @@ class DatabaseUserHistory implements UserHistoryStore
         if ( auditRecord instanceof HelpdeskAuditRecord && auditRecord.getType() == AuditEvent.Type.HELPDESK )
         {
             final HelpdeskAuditRecord helpdeskAuditRecord = ( HelpdeskAuditRecord ) auditRecord;
-            userIdentity = new UserIdentity( helpdeskAuditRecord.getTargetDN(), helpdeskAuditRecord.getTargetLdapProfile() );
+            userIdentity = UserIdentity.createUserIdentity( helpdeskAuditRecord.getTargetDN(), helpdeskAuditRecord.getTargetLdapProfile() );
         }
         else
         {
-            userIdentity = new UserIdentity( auditRecord.getPerpetratorDN(), auditRecord.getPerpetratorLdapProfile() );
+            userIdentity = UserIdentity.createUserIdentity( auditRecord.getPerpetratorDN(), auditRecord.getPerpetratorLdapProfile() );
         }
 
         final String guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, null, userIdentity, false );

+ 2 - 2
server/src/main/java/password/pwm/svc/event/LdapXmlUserHistory.java

@@ -102,11 +102,11 @@ class LdapXmlUserHistory implements UserHistoryStore
         if ( auditRecord instanceof HelpdeskAuditRecord && auditRecord.getType() == AuditEvent.Type.HELPDESK )
         {
             final HelpdeskAuditRecord helpdeskAuditRecord = ( HelpdeskAuditRecord ) auditRecord;
-            userIdentity = new UserIdentity( helpdeskAuditRecord.getTargetDN(), helpdeskAuditRecord.getTargetLdapProfile() );
+            userIdentity = UserIdentity.createUserIdentity( helpdeskAuditRecord.getTargetDN(), helpdeskAuditRecord.getTargetLdapProfile() );
         }
         else
         {
-            userIdentity = new UserIdentity( auditRecord.getPerpetratorDN(), auditRecord.getPerpetratorLdapProfile() );
+            userIdentity = UserIdentity.createUserIdentity( auditRecord.getPerpetratorDN(), auditRecord.getPerpetratorLdapProfile() );
         }
         final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
 

+ 2 - 50
server/src/main/java/password/pwm/util/BasicAuthInfo.java

@@ -20,6 +20,7 @@
 
 package password.pwm.util;
 
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -38,9 +39,9 @@ import java.io.Serializable;
  *
  * @author Jason D. Rivard
  */
+@Value
 public class BasicAuthInfo implements Serializable
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( BasicAuthInfo.class );
 
     private final String username;
@@ -138,54 +139,5 @@ public class BasicAuthInfo implements Serializable
 
         return sb.toString();
     }
-
-    public BasicAuthInfo(
-            final String username,
-            final PasswordData password
-    )
-    {
-        this.username = username;
-        this.password = password;
-    }
-
-    public PasswordData getPassword( )
-    {
-        return password;
-    }
-
-    public String getUsername( )
-    {
-        return username;
-    }
-
-    public boolean equals( final Object o )
-    {
-        if ( this == o )
-        {
-            return true;
-        }
-        if ( !( o instanceof BasicAuthInfo ) )
-        {
-            return false;
-        }
-
-        final BasicAuthInfo basicAuthInfo = ( BasicAuthInfo ) o;
-
-        return !( password != null
-                ? !password.equals( basicAuthInfo.password )
-                : basicAuthInfo.password != null )
-                &&
-                !( username != null
-                        ? !username.equals( basicAuthInfo.username )
-                        : basicAuthInfo.username != null );
-    }
-
-    public int hashCode( )
-    {
-        int result;
-        result = ( username != null ? username.hashCode() : 0 );
-        result = 29 * result + ( password != null ? password.hashCode() : 0 );
-        return result;
-    }
 }
 

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

@@ -77,7 +77,7 @@ public class SampleDataGenerator
         );
         responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941246275 ) );
 
-        final UserIdentity userIdentity = new UserIdentity( "cn=FLast,ou=test,o=org", "profile1" );
+        final UserIdentity userIdentity = UserIdentity.createUserIdentity( "cn=FLast,ou=test,o=org", "profile1" );
 
         return UserInfoBean.builder()
                 .userIdentity( userIdentity )
@@ -128,7 +128,7 @@ public class SampleDataGenerator
             );
             responseInfoBean.setTimestamp( Instant.ofEpochSecond( 941244474 ) );
 
-            final UserIdentity userIdentity = new UserIdentity( "cn=TUser,ou=test,o=org", "profile1" );
+            final UserIdentity userIdentity = UserIdentity.createUserIdentity( "cn=TUser,ou=test,o=org", "profile1" );
 
             return UserInfoBean.builder()
                     .userIdentity( userIdentity )

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

@@ -682,7 +682,7 @@ public class JavaHelper
 
     public static <E extends Enum<E>> EnumSet<E> copiedEnumSet( final Collection<E> source, final Class<E> classOfT )
     {
-        return source == null || source.isEmpty()
+        return JavaHelper.isEmpty( source )
                 ? EnumSet.noneOf( classOfT )
                 : EnumSet.copyOf( source );
     }

+ 14 - 1
server/src/main/java/password/pwm/util/logging/PwmLogEvent.java

@@ -35,6 +35,7 @@ import java.io.Serializable;
 import java.io.StringWriter;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 @Value
@@ -52,6 +53,18 @@ public class PwmLogEvent implements Serializable, Comparable<PwmLogEvent>
     private final String username;
     private final String sourceAddress;
 
+    private static final Comparator<PwmLogEvent> COMPARATOR = Comparator.comparing(
+            PwmLogEvent::getTimestamp,
+            Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    PwmLogEvent::getSessionID,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    PwmLogEvent::getRequestID,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing( PwmLogEvent::getLevel,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) );
+
 
     public static PwmLogEvent fromEncodedString( final String encodedString )
             throws ClassNotFoundException, IOException
@@ -144,7 +157,7 @@ public class PwmLogEvent implements Serializable, Comparable<PwmLogEvent>
     @Override
     public int compareTo( final PwmLogEvent o )
     {
-        return this.getTimestamp().compareTo( o.getTimestamp() );
+        return COMPARATOR.compare( this, o );
     }
 
     String toEncodedString( )

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

@@ -426,7 +426,7 @@ public class PasswordUtility
         {
             final ChaiUser theUser = chaiProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
             bindDN = chaiProvider.getChaiConfiguration().getSetting( ChaiSetting.BIND_DN );
-            bindIsSelf = userIdentity.canonicalEquals( new UserIdentity( bindDN, userIdentity.getLdapProfileID() ), pwmApplication );
+            bindIsSelf = userIdentity.canonicalEquals( UserIdentity.createUserIdentity( bindDN, userIdentity.getLdapProfileID() ), pwmApplication );
 
             LOGGER.trace( sessionLabel, () -> "preparing to setActorPassword for '" + theUser.getEntryDN() + "', using bind DN: " + bindDN );
 

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

@@ -4106,7 +4106,7 @@
     <setting hidden="false" key="pwm.seedlist.location" level="2">
         <default/>
     </setting>
-    <!-- DEPRECATED SETTINGS -->
+    <!-- BEGIN DEPRECATED SETTINGS -->
     <setting hidden="true" key="password.policy.ADComplexity" level="99" required="false">
         <flag>Deprecated</flag>
         <default>
@@ -4146,233 +4146,233 @@
             <value>false</value>
         </default>
     </setting>
-    <!-- DEPRECATED SETTINGS -->
-    <category hidden="false" key="TEMPLATES">
+    <!-- END DEPRECATED SETTINGS -->
+    <category hidden="false" scope="DOMAIN" key="TEMPLATES">
     </category>
-    <category hidden="false" key="NOTES">
+    <category hidden="false" scope="DOMAIN" key="NOTES">
     </category>
-    <category hidden="false" key="SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="SETTINGS">
     </category>
-    <category hidden="false" key="PROFILES">
+    <category hidden="false" scope="DOMAIN" key="PROFILES">
     </category>
-    <category hidden="false" key="MODULES">
+    <category hidden="false" scope="DOMAIN" key="MODULES">
     </category>
-    <category hidden="false" key="HTTPS_SERVER">
+    <category hidden="false" scope="DOMAIN" key="HTTPS_SERVER">
     </category>
-    <category hidden="false" key="LDAP">
+    <category hidden="false" scope="DOMAIN" key="LDAP">
     </category>
-    <category hidden="false" key="LDAP_PROFILE" profiles="true">
+    <category hidden="false" scope="DOMAIN" key="LDAP_PROFILE">
         <profile setting="ldap.profile.list"/>
     </category>
-    <category hidden="false" key="LDAP_BASE">
+    <category hidden="false" scope="DOMAIN" key="LDAP_BASE">
     </category>
-    <category hidden="false" key="LDAP_ATTRIBUTES">
+    <category hidden="false" scope="DOMAIN" key="LDAP_ATTRIBUTES">
     </category>
-    <category hidden="false" key="LDAP_LOGIN">
+    <category hidden="false" scope="DOMAIN" key="LDAP_LOGIN">
     </category>
-    <category hidden="false" key="LDAP_GLOBAL">
+    <category hidden="false" scope="DOMAIN" key="LDAP_GLOBAL">
     </category>
-    <category hidden="false" key="LDAP_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="LDAP_SETTINGS">
     </category>
-    <category hidden="false" key="APPLICATION">
+    <category hidden="false" scope="DOMAIN" key="APPLICATION">
     </category>
-    <category hidden="false" key="GENERAL">
+    <category hidden="false" scope="DOMAIN" key="GENERAL">
     </category>
-    <category hidden="false" key="LOCALIZATION">
+    <category hidden="false" scope="DOMAIN" key="LOCALIZATION">
     </category>
-    <category hidden="false" key="TELEMETRY">
+    <category hidden="false" scope="DOMAIN" key="TELEMETRY">
     </category>
-    <category hidden="false" key="CLUSTERING">
+    <category hidden="false" scope="DOMAIN" key="CLUSTERING">
     </category>
-    <category hidden="false" key="USER_INTERFACE">
+    <category hidden="false" scope="DOMAIN" key="USER_INTERFACE">
     </category>
-    <category hidden="false" key="UI_FEATURES">
+    <category hidden="false" scope="DOMAIN" key="UI_FEATURES">
     </category>
-    <category hidden="false" key="UI_WEB">
+    <category hidden="false" scope="DOMAIN" key="UI_WEB">
     </category>
-    <category hidden="false" key="PASSWORD_POLICY">
+    <category hidden="false" scope="DOMAIN" key="PASSWORD_POLICY">
         <profile setting="password.profile.list"/>
     </category>
-    <category hidden="false" key="PASSWORD_GLOBAL">
+    <category hidden="false" scope="DOMAIN" key="PASSWORD_GLOBAL">
     </category>
-    <category hidden="false" key="CHALLENGE">0
+    <category hidden="false" scope="DOMAIN" key="CHALLENGE">0
     </category>
-    <category hidden="false" key="CHALLENGE_POLICY">
+    <category hidden="false" scope="DOMAIN" key="CHALLENGE_POLICY">
         <profile setting="challenge.profile.list"/>
     </category>
-    <category hidden="false" key="EMAIL_SERVERS">
+    <category hidden="false" scope="DOMAIN" key="EMAIL_SERVERS">
         <profile setting="email.profile.list"/>
     </category>
-    <category hidden="false" key="EMAIL">
+    <category hidden="false" scope="DOMAIN" key="EMAIL">
     </category>
-    <category hidden="false" key="EMAIL_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="EMAIL_SETTINGS">
     </category>
-    <category hidden="false" key="EMAIL_TEMPLATES">
+    <category hidden="false" scope="DOMAIN" key="EMAIL_TEMPLATES">
     </category>
-    <category hidden="false" key="SMS">
+    <category hidden="false" scope="DOMAIN" key="SMS">
     </category>
-    <category hidden="false" key="SMS_GATEWAY">
+    <category hidden="false" scope="DOMAIN" key="SMS_GATEWAY">
     </category>
-    <category hidden="false" key="SMS_MESSAGES">
+    <category hidden="false" scope="DOMAIN" key="SMS_MESSAGES">
     </category>
-    <category hidden="false" key="SECURITY">
+    <category hidden="false" scope="DOMAIN" key="SECURITY">
     </category>
-    <category hidden="false" key="WORDLISTS">
+    <category hidden="false" scope="SYSTEM" key="WORDLISTS">
     </category>
-    <category hidden="false" key="APP_SECURITY">
+    <category hidden="false" scope="DOMAIN" key="APP_SECURITY">
     </category>
-    <category hidden="false" key="WEB_SECURITY">
+    <category hidden="false" scope="DOMAIN" key="WEB_SECURITY">
     </category>
-    <category hidden="false" key="CAPTCHA">
+    <category hidden="false" scope="DOMAIN" key="CAPTCHA">
     </category>
-    <category hidden="false" key="INTRUDER">
+    <category hidden="false" scope="DOMAIN" key="INTRUDER">
     </category>
-    <category hidden="false" key="INTRUDER_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="INTRUDER_SETTINGS">
     </category>
-    <category hidden="false" key="INTRUDER_TIMEOUTS">
+    <category hidden="false" scope="DOMAIN" key="INTRUDER_TIMEOUTS">
     </category>
-    <category hidden="false" key="TOKEN">
+    <category hidden="false" scope="DOMAIN" key="TOKEN">
     </category>
-    <category hidden="false" key="OTP_SETUP">
+    <category hidden="false" scope="DOMAIN" key="OTP_SETUP">
     </category>
-    <category hidden="false" key="OTP_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="OTP_SETTINGS">
     </category>
-    <category hidden="false" key="OTP_PROFILE" profiles="true">
+    <category hidden="false" scope="DOMAIN" key="OTP_PROFILE">
         <profile setting="otp.profile.list"/>
     </category>
-    <category hidden="false" key="LOGGING">
+    <category hidden="false" scope="DOMAIN"  key="LOGGING">
     </category>
-    <category hidden="false" key="AUDITING">
+    <category hidden="false" scope="DOMAIN" key="AUDITING">
     </category>
-    <category hidden="false" key="AUDIT_CONFIG">
+    <category hidden="false" scope="DOMAIN" key="AUDIT_CONFIG">
     </category>
-    <category hidden="false" key="USER_HISTORY">
+    <category hidden="false" scope="DOMAIN" key="USER_HISTORY">
     </category>
-    <category hidden="false" key="AUDIT_FORWARD">
+    <category hidden="false" scope="DOMAIN" key="AUDIT_FORWARD">
     </category>
-    <category hidden="false" key="EDIRECTORY">
+    <category hidden="false" scope="DOMAIN" key="EDIRECTORY">
     </category>
-    <category hidden="false" key="EDIR_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="EDIR_SETTINGS">
     </category>
-    <category hidden="false" key="EDIR_CR_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="EDIR_CR_SETTINGS">
     </category>
-    <category hidden="false" key="ACTIVE_DIRECTORY">
+    <category hidden="false" scope="DOMAIN" key="ACTIVE_DIRECTORY">
     </category>
-    <category hidden="false" key="ORACLE_DS">
+    <category hidden="false" scope="DOMAIN" key="ORACLE_DS">
     </category>
-    <category hidden="false" key="DATABASE">
+    <category hidden="false" scope="DOMAIN" key="DATABASE">
     </category>
-    <category hidden="false" key="DATABASE_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="DATABASE_SETTINGS">
     </category>
-    <category hidden="false" key="DATABASE_ADV">
+    <category hidden="false" scope="DOMAIN" key="DATABASE_ADV">
     </category>
-    <category hidden="false" key="REPORTING">
+    <category hidden="false" scope="DOMAIN" key="REPORTING">
     </category>
-    <category hidden="false" key="PW_EXP_NOTIFY">
+    <category hidden="false" scope="DOMAIN" key="PW_EXP_NOTIFY">
     </category>
-    <category hidden="false" key="WEB_SERVICES" level="2">
+    <category hidden="false" scope="DOMAIN" key="WEB_SERVICES" level="2">
     </category>
-    <category hidden="false" key="REST_SERVER" level="2">
+    <category hidden="false" scope="DOMAIN" key="REST_SERVER" level="2">
     </category>
-    <category hidden="false" key="REST_CLIENT" level="2">
+    <category hidden="false" scope="DOMAIN" key="REST_CLIENT" level="2">
     </category>
-    <category hidden="false" key="SSO" level="2">
+    <category hidden="false" scope="DOMAIN" key="SSO" level="2">
     </category>
-    <category hidden="false" key="OAUTH" level="2">
+    <category hidden="false" scope="DOMAIN" key="OAUTH" level="2">
     </category>
-    <category hidden="false" key="HTTP_SSO" level="2">
+    <category hidden="false" scope="DOMAIN" key="HTTP_SSO" level="2">
     </category>
-    <category hidden="false" key="CAS_SSO" level="2">
+    <category hidden="false" scope="DOMAIN" key="CAS_SSO" level="2">
     </category>
-    <category hidden="false" key="BASIC_SSO">
+    <category hidden="false" scope="DOMAIN" key="BASIC_SSO">
     </category>
-    <category hidden="false" key="CHANGE_PASSWORD">
+    <category hidden="false" scope="DOMAIN" key="CHANGE_PASSWORD">
     </category>
-    <category hidden="false" key="CHANGE_PASSWORD_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="CHANGE_PASSWORD_SETTINGS">
     </category>
-    <category hidden="false" key="CHANGE_PASSWORD_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="CHANGE_PASSWORD_PROFILE">
         <profile setting="accountInfo.profile.list"/>
     </category>
-    <category hidden="false" key="ACCOUNT_INFO">
+    <category hidden="false" scope="DOMAIN" key="ACCOUNT_INFO">
     </category>
-    <category hidden="false" key="ACCOUNT_INFO_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="ACCOUNT_INFO_SETTINGS">
     </category>
-    <category hidden="false" key="ACCOUNT_INFO_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="ACCOUNT_INFO_PROFILE">
         <profile setting="accountInfo.profile.list"/>
     </category>
-    <category hidden="false" key="RECOVERY">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY">
     </category>
-    <category hidden="false" key="RECOVERY_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY_SETTINGS">
     </category>
-    <category hidden="false" key="RECOVERY_OPTIONS">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY_OPTIONS">
     </category>
-    <category hidden="false" key="RECOVERY_PROFILE" profiles="true">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY_PROFILE">
         <profile setting="recovery.profile.list"/>
     </category>
-    <category hidden="false" key="RECOVERY_DEF">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY_DEF">
     </category>
-    <category hidden="false" key="RECOVERY_OAUTH">
+    <category hidden="false" scope="DOMAIN" key="RECOVERY_OAUTH">
     </category>
-    <category hidden="false" key="ADMINISTRATION">
+    <category hidden="false" scope="DOMAIN" key="ADMINISTRATION">
     </category>
-    <category hidden="false" key="FORGOTTEN_USERNAME">
+    <category hidden="false" scope="DOMAIN" key="FORGOTTEN_USERNAME">
     </category>
-    <category hidden="false" key="NEWUSER">
+    <category hidden="false" scope="DOMAIN" key="NEWUSER">
     </category>
-    <category hidden="false" key="NEWUSER_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="NEWUSER_SETTINGS">
     </category>
-    <category hidden="false" key="NEWUSER_PROFILE" profiles="true">
+    <category hidden="false" scope="DOMAIN" key="NEWUSER_PROFILE" profiles="true">
         <profile setting="newUser.profile.list"/>
     </category>
-    <category hidden="false" key="GUEST">
+    <category hidden="false" scope="DOMAIN" key="GUEST">
     </category>
-    <category hidden="false" key="ACTIVATION">
+    <category hidden="false" scope="DOMAIN" key="ACTIVATION">
     </category>
-    <category hidden="false" key="ACTIVATION_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="ACTIVATION_SETTINGS">
     </category>
-    <category hidden="false" key="ACTIVATION_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="ACTIVATION_PROFILE">
         <profile setting="activateUser.profile.list"/>
     </category>
-    <category hidden="false" key="UPDATE">
+    <category hidden="false" scope="DOMAIN" key="UPDATE">
     </category>
-    <category hidden="false" key="UPDATE_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="UPDATE_SETTINGS">
     </category>
-    <category hidden="false" key="UPDATE_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="UPDATE_PROFILE">
         <profile setting="updateAttributes.profile.list"/>
     </category>
-    <category hidden="false" key="SHORTCUT">
+    <category hidden="false" scope="DOMAIN" key="SHORTCUT">
     </category>
-    <category hidden="false" key="PEOPLE_SEARCH">
+    <category hidden="false" scope="DOMAIN" key="PEOPLE_SEARCH">
     </category>
-    <category hidden="false" key="PEOPLE_SEARCH_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="PEOPLE_SEARCH_SETTINGS">
     </category>
-    <category hidden="false" key="PEOPLE_SEARCH_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="PEOPLE_SEARCH_PROFILE">
         <profile setting="peopleSearch.profile.list"/>
     </category>
-    <category hidden="false" key="HELPDESK">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK">
     </category>
-    <category hidden="false" key="HELPDESK_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK_PROFILE">
         <profile setting="helpdesk.profile.list"/>
     </category>
-    <category hidden="false" key="HELPDESK_OPTIONS">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK_OPTIONS">
     </category>
-    <category hidden="false" key="HELPDESK_BASE">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK_BASE">
     </category>
-    <category hidden="false" key="HELPDESK_VERIFICATION">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK_VERIFICATION">
     </category>
-    <category hidden="false" key="HELPDESK_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="HELPDESK_SETTINGS">
     </category>
-    <category hidden="false" key="MODULES_PUBLIC">
+    <category hidden="false" scope="DOMAIN" key="MODULES_PUBLIC">
     </category>
-    <category hidden="false" key="MODULES_PRIVATE">
+    <category hidden="false" scope="DOMAIN" key="MODULES_PRIVATE">
     </category>
-    <category hidden="false" key="DELETE_ACCOUNT">
+    <category hidden="false" scope="DOMAIN" key="DELETE_ACCOUNT">
     </category>
-    <category hidden="false" key="DELETE_ACCOUNT_SETTINGS">
+    <category hidden="false" scope="DOMAIN" key="DELETE_ACCOUNT_SETTINGS">
     </category>
-    <category hidden="false" key="DELETE_ACCOUNT_PROFILE">
+    <category hidden="false" scope="DOMAIN" key="DELETE_ACCOUNT_PROFILE">
         <profile setting="deleteAccount.profile.list"/>
     </category>
-    <category hidden="true" key="INTERNAL">
+    <category hidden="true" scope="DOMAIN" key="INTERNAL">
     </category>
 </settings>

+ 11 - 1
server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java

@@ -76,11 +76,21 @@ public class PwmSettingCategoryTest
         {
             if ( category.hasProfiles() )
             {
-                final boolean hasChildren = !category.getChildCategories().isEmpty();
+                final boolean hasChildren = !category.getChildren().isEmpty();
                 final boolean hasSettings = !category.getSettings().isEmpty();
                 Assert.assertTrue( hasChildren || hasSettings );
                 Assert.assertFalse( category.getKey() + " has both child categories and settings", hasChildren && hasSettings );
             }
         }
     }
+
+    @Test
+    public void testScope()
+    {
+        for ( final PwmSettingCategory category : PwmSettingCategory.values() )
+        {
+                final PwmSettingScope scope = category.getScope();
+                Assert.assertNotNull( scope );
+        }
+    }
 }

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

@@ -201,7 +201,7 @@ public class PwmSettingTest
     @Test
     public void sortedByMenuLocation()
     {
-        final Set<PwmSetting> sortedSet = PwmSetting.sortedByMenuLocation( PwmConstants.DEFAULT_LOCALE );
-        Assert.assertEquals( sortedSet.size(), PwmSetting.values().length );
+        final List<PwmSetting> list = PwmSetting.sortedValues();
+        Assert.assertEquals( list.size(), PwmSetting.values().length );
     }
 }

+ 2 - 1
server/src/test/java/password/pwm/config/stored/StoredConfigItemKeyTest.java

@@ -22,6 +22,7 @@ package password.pwm.config.stored;
 
 import org.junit.Assert;
 import org.junit.Test;
+import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Display;
@@ -102,7 +103,7 @@ public class StoredConfigItemKeyTest
         }
 
         Collections.shuffle( list );
-        Collections.sort( list );
+        list.sort( StoredConfigItemKey.comparator( PwmConstants.DEFAULT_LOCALE ) );
         //System.out.println( list.size() );
         //list.forEach( System.out::println );
     }

+ 46 - 0
server/src/test/java/password/pwm/health/HealthStatusTest.java

@@ -0,0 +1,46 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Test;
+
+import java.util.EnumSet;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+public class HealthStatusTest
+{
+
+    @Test
+    public void mostSevere()
+    {
+        final Set<HealthStatus> set = EnumSet.noneOf( HealthStatus.class );
+        set.add( HealthStatus.GOOD );
+        set.add( HealthStatus.CAUTION );
+        set.add( HealthStatus.WARN );
+        set.add( HealthStatus.CONFIG );
+        Assert.assertEquals( HealthStatus.WARN, HealthStatus.mostSevere( set ).orElseThrow( NoSuchElementException::new ) );
+
+        set.remove( HealthStatus.WARN );
+        Assert.assertEquals( HealthStatus.CAUTION, HealthStatus.mostSevere( set ).orElseThrow( NoSuchElementException::new ) );
+    }
+}

+ 7 - 5
webapp/src/main/webapp/public/reference/settings.jsp

@@ -34,6 +34,7 @@
 <%@ page import="password.pwm.util.macro.MacroRequest" %>
 <%@ page import="com.novell.ldapchai.util.StringHelper" %>
 <%@ page import="password.pwm.AppProperty" %>
+<%@ page import="password.pwm.config.PwmSettingStats" %>
 
 <!DOCTYPE html>
 <% JspUtility.setFlag(pageContext, PwmRequestFlag.HIDE_HEADER_WARNINGS); %>
@@ -48,7 +49,7 @@
 <%
     final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext);
     final boolean advancedMode = false;
-    final List<PwmSettingCategory> sortedCategories = PwmSettingCategory.valuesForReferenceDoc(userLocale);
+    final List<PwmSettingCategory> sortedCategories = PwmSettingCategory.valuesForReferenceDoc();
     final MacroRequest macroRequest = MacroRequest.forNonUserSpecific(pwmRequest.getPwmApplication(), pwmRequest.getLabel());
 %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
@@ -227,14 +228,14 @@
         <% } %>
         <% if (advancedMode) { %>
         <h2><a id="settingStatistics">Setting Statistics</a></h2>
-        <% final Map<PwmSetting.SettingStat,Object> settingStats = PwmSetting.getStats(); %>
+        <% final Map<PwmSettingStats.SettingStat,Object> settingStats = PwmSettingStats.getStats(); %>
         <table>
             <tr>
                 <td>
                     Total Settings
                 </td>
                 <td>
-                    <%= settingStats.get(PwmSetting.SettingStat.Total) %>
+                    <%= settingStats.get( PwmSettingStats.SettingStat.Total) %>
                 </td>
             </tr>
             <tr>
@@ -242,10 +243,11 @@
                     Settings that are part of a Profile
                 </td>
                 <td>
-                    <%= settingStats.get(PwmSetting.SettingStat.hasProfile) %>
+                    <%= settingStats.get( PwmSettingStats.SettingStat.hasProfile) %>
                 </td>
             </tr>
-            <% final Map<PwmSettingSyntax,Integer> syntaxCounts = (Map<PwmSettingSyntax,Integer>)settingStats.get(PwmSetting.SettingStat.syntaxCounts); %>
+            <% final Map<PwmSettingSyntax,Integer> syntaxCounts = (Map<PwmSettingSyntax,Integer>)settingStats.get(
+                    PwmSettingStats.SettingStat.syntaxCounts); %>
             <% for (final Map.Entry<PwmSettingSyntax,Integer> entry : syntaxCounts.entrySet()) { %>
             <tr>
                 <td>