فهرست منبع

Merge pull request #241 from pwm-project/jrivard-bugs

bug fixes
Jason 7 سال پیش
والد
کامیت
abdb76c446
34فایلهای تغییر یافته به همراه587 افزوده شده و 658 حذف شده
  1. 27 11
      server/src/main/java/password/pwm/config/Configuration.java
  2. 4 2
      server/src/main/java/password/pwm/config/PwmSetting.java
  3. 2 2
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  4. 26 7
      server/src/main/java/password/pwm/config/option/MessageSendMethod.java
  5. 15 8
      server/src/main/java/password/pwm/config/profile/AbstractProfile.java
  6. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  7. 75 1
      server/src/main/java/password/pwm/health/ConfigurationChecker.java
  8. 1 0
      server/src/main/java/password/pwm/health/HealthMessage.java
  9. 7 163
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  10. 1 14
      server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java
  11. 0 24
      server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  12. 9 33
      server/src/main/java/password/pwm/http/servlet/configeditor/CategoryInfo.java
  13. 143 208
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  14. 59 0
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java
  15. 4 54
      server/src/main/java/password/pwm/http/servlet/configeditor/SettingInfo.java
  16. 2 2
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  17. 1 1
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  18. 2 2
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  19. 4 0
      server/src/main/java/password/pwm/i18n/Admin.java
  20. 6 1
      server/src/main/java/password/pwm/i18n/PwmSetting.java
  21. 9 1
      server/src/main/java/password/pwm/svc/event/AuditService.java
  22. 7 2
      server/src/main/java/password/pwm/svc/event/SyslogAuditService.java
  23. 1 1
      server/src/main/java/password/pwm/svc/stats/EpsStatistic.java
  24. 5 7
      server/src/main/java/password/pwm/svc/stats/Statistic.java
  25. 6 39
      server/src/main/java/password/pwm/svc/token/TokenService.java
  26. 1 29
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  27. 22 23
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  28. 13 9
      server/src/main/resources/password/pwm/i18n/Admin.properties
  29. 1 0
      server/src/main/resources/password/pwm/i18n/Health.properties
  30. 5 10
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  31. 2 2
      server/src/main/webapp/index.jsp
  32. 4 1
      server/src/main/webapp/public/reference/settings.jsp
  33. 52 0
      server/src/test/java/password/pwm/config/PwmSettingPropertyTest.java
  34. 70 0
      server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java

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

@@ -109,6 +109,15 @@ public class Configuration implements SettingReader {
         this.storedConfiguration = storedConfiguration;
     }
 
+    public static void deprecatedSettingException(final PwmSetting pwmSetting, final String profile, final MessageSendMethod value)
+    {
+        if (value != null && value.isDeprecated()) {
+            final String msg = pwmSetting.toMenuLocationDebug(profile, PwmConstants.DEFAULT_LOCALE)
+                    + " setting is using a no longer functional setting value: " + value;
+            throw new IllegalStateException(msg);
+        }
+    }
+
     public String toDebugString() {
         final StringBuilder outputText = new StringBuilder();
         outputText.append("  ");
@@ -162,7 +171,12 @@ public class Configuration implements SettingReader {
 
     public <E extends Enum<E>> E readSettingAsEnum(final PwmSetting setting, final Class<E> enumClass) {
         final StoredValue value = readStoredValue(setting);
-        return JavaTypeConverter.valueToEnum(setting, value, enumClass);
+        final E returnValue =  JavaTypeConverter.valueToEnum(setting, value, enumClass);
+        if (MessageSendMethod.class.equals(enumClass)) {
+            deprecatedSettingException(setting, null, (MessageSendMethod) returnValue);
+        }
+
+        return returnValue;
     }
 
     public <E extends Enum<E>> Set<E> readSettingAsOptionList(final PwmSetting setting, final Class<E> enumClass) {
@@ -371,15 +385,17 @@ public class Configuration implements SettingReader {
                 throw new IllegalArgumentException("may not read SELECT enum value for setting: " + setting.toString());
             }
 
-            final String strValue = (String)value.toNativeObject();
-            try {
-                return (E)enumClass.getMethod("valueOf", String.class).invoke(null, strValue);
-            } catch (InvocationTargetException e1) {
-                if (e1.getCause() instanceof IllegalArgumentException) {
-                    LOGGER.error("illegal setting value for option '" + strValue + "' for setting key '" + setting.getKey() + "' is not recognized, will use default");
+            if (value != null) {
+                final String strValue = (String) value.toNativeObject();
+                try {
+                    return (E) enumClass.getMethod("valueOf", String.class).invoke(null, strValue);
+                } catch (InvocationTargetException e1) {
+                    if (e1.getCause() instanceof IllegalArgumentException) {
+                        LOGGER.error("illegal setting value for option '" + strValue + "' for setting key '" + setting.getKey() + "' is not recognized, will use default");
+                    }
+                } catch (Exception e1) {
+                    LOGGER.error("unexpected error", e1);
                 }
-            } catch (Exception e1) {
-                LOGGER.error("unexpected error", e1);
             }
 
             return null;
@@ -907,7 +923,7 @@ public class Configuration implements SettingReader {
             case ForgottenPassword:
                 newProfile = ForgottenPasswordProfile.makeFromStoredConfiguration(storedConfiguration, profileID);
                 break;
-            
+
             case NewUser:
                 newProfile = NewUserProfile.makeFromStoredConfiguration(storedConfiguration, profileID);
                 break;
@@ -937,7 +953,7 @@ public class Configuration implements SettingReader {
     }
 
     public String configurationHash()
-            throws PwmUnrecoverableException 
+            throws PwmUnrecoverableException
     {
         if (this.cashedConfigurationHash == null) {
             this.cashedConfigurationHash = storedConfiguration.settingChecksum();

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

@@ -80,6 +80,8 @@ public enum PwmSetting {
             "pwm.logoutURL", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL),
     URL_HOME(
             "pwm.homeURL", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL),
+    URL_INTRO(
+            "pwm.introURL", PwmSettingSyntax.SELECT, PwmSettingCategory.GENERAL),
     PWM_INSTANCE_NAME(
             "pwmInstanceName", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL),
     IDLE_TIMEOUT_SECONDS(
@@ -1359,12 +1361,12 @@ public enum PwmSetting {
     }
 
     public String getLabel(final Locale locale) {
-        final String propertyKey = "Setting_Label_" + this.getKey();
+        final String propertyKey = password.pwm.i18n.PwmSetting.SETTING_LABEL_PREFIX + this.getKey();
         return LocaleHelper.getLocalizedMessage(locale, propertyKey, null, password.pwm.i18n.PwmSetting.class);
     }
 
     public String getDescription(final Locale locale) {
-        final String propertyKey = "Setting_Description_" + this.getKey();
+        final String propertyKey = password.pwm.i18n.PwmSetting.SETTING_DESCRIPTION_PREFIX + this.getKey();
         final String storedText = LocaleHelper.getLocalizedMessage(locale, propertyKey, null, password.pwm.i18n.PwmSetting.class);
         final MacroMachine macroMachine = MacroMachine.forStatic();
         return macroMachine.expandMacros(storedText);

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

@@ -212,12 +212,12 @@ public enum PwmSettingCategory {
     }
 
     public String getLabel(final Locale locale) {
-        final String key = "Category_Label_" + this.getKey();
+        final String key = PwmSetting.CATEGORY_LABEL_PREFIX + this.getKey();
         return LocaleHelper.getLocalizedMessage(locale, key, null, PwmSetting.class);
     }
 
     public String getDescription(final Locale locale) {
-        final String key = "Category_Description_" + this.getKey();
+        final String key = PwmSetting.CATEGORY_DESCRIPTION_PREFIX + this.getKey();
         return LocaleHelper.getLocalizedMessage(locale, key, null, PwmSetting.class);
     }
 

+ 26 - 7
server/src/main/java/password/pwm/config/option/MessageSendMethod.java

@@ -23,11 +23,30 @@
 package password.pwm.config.option;
 
 public enum MessageSendMethod implements ConfigurationOption {
-    NONE,
-    EMAILONLY,
-    BOTH,
-    EMAILFIRST,
-    SMSFIRST,
-    SMSONLY,
-    CHOICE_SMS_EMAIL,
+    NONE(false),
+
+    EMAILONLY(false),
+    SMSONLY(false),
+    CHOICE_SMS_EMAIL(false),
+
+    @Deprecated
+    BOTH(true),         //deprecated oct 2017
+
+    @Deprecated
+    EMAILFIRST(true),   //deprecated oct 2017
+
+    @Deprecated
+    SMSFIRST(true),     //deprecated oct 2017
+
+    ;
+
+    private final boolean deprecated;
+
+    MessageSendMethod(final boolean deprecated) {
+        this.deprecated = deprecated;
+    }
+
+    public boolean isDeprecated() {
+        return deprecated;
+    }
 }

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

@@ -22,18 +22,19 @@
 
 package password.pwm.config.profile;
 
-import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.Configuration;
-import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.SettingReader;
 import password.pwm.config.StoredValue;
-import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
+import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.config.value.data.UserPermission;
 import password.pwm.util.PasswordData;
 
 import java.security.cert.X509Certificate;
@@ -49,10 +50,10 @@ import java.util.Set;
 
 public abstract class AbstractProfile implements Profile, SettingReader {
 
-    protected final String identifier;
-    protected final Map<PwmSetting,StoredValue> storedValueMap;
+    private final String identifier;
+    final Map<PwmSetting,StoredValue> storedValueMap;
 
-    protected AbstractProfile(final String identifier, final Map<PwmSetting, StoredValue> storedValueMap) {
+    AbstractProfile(final String identifier, final Map<PwmSetting, StoredValue> storedValueMap) {
         this.identifier = identifier;
         this.storedValueMap = storedValueMap;
     }
@@ -88,7 +89,13 @@ public abstract class AbstractProfile implements Profile, SettingReader {
 
     @Override
     public <E extends Enum<E>> E readSettingAsEnum(final PwmSetting setting, final Class<E> enumClass) {
-        return Configuration.JavaTypeConverter.valueToEnum(setting, storedValueMap.get(setting), enumClass);
+        final StoredValue value = storedValueMap.get(setting);
+        final E returnValue =  Configuration.JavaTypeConverter.valueToEnum(setting, value, enumClass);
+        if (MessageSendMethod.class.equals(enumClass)) {
+            Configuration.deprecatedSettingException(setting, this.getIdentifier(), (MessageSendMethod) returnValue);
+        }
+
+        return returnValue;
     }
 
     public List<ActionConfiguration> readSettingAsAction(final PwmSetting setting) {
@@ -150,7 +157,7 @@ public abstract class AbstractProfile implements Profile, SettingReader {
         return valueMap;
     }
 
-    public Set<IdentityVerificationMethod> readVerificationMethods(final PwmSetting pwmSetting, final VerificationMethodValue.EnabledState enabledState) {
+    Set<IdentityVerificationMethod> readVerificationMethods(final PwmSetting pwmSetting, final VerificationMethodValue.EnabledState enabledState) {
         final Set<IdentityVerificationMethod> result = new LinkedHashSet<>();
         final StoredValue configValue = storedValueMap.get(pwmSetting);
         final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();

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

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

+ 75 - 1
server/src/main/java/password/pwm/health/ConfigurationChecker.java

@@ -31,6 +31,9 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.option.DataStorageMethod;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.profile.ForgottenPasswordProfile;
+import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
@@ -49,6 +52,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 public class ConfigurationChecker implements HealthChecker {
     private static final PwmLogger LOGGER = PwmLogger.forClass(ConfigurationChecker.class);
@@ -224,7 +229,8 @@ public class ConfigurationChecker implements HealthChecker {
     private static final List<Class<? extends ConfigHealthCheck>> ALL_CHECKS = Collections.unmodifiableList(Arrays.asList(
             VerifyPasswordPolicyConfigs.class,
             VerifyResponseLdapAttribute.class,
-            VerifyDbConfiguredIfNeeded.class
+            VerifyDbConfiguredIfNeeded.class,
+            VerifyIfDeprecatedSendMethodValuesUsed.class
     ));
 
     static class VerifyResponseLdapAttribute implements ConfigHealthCheck {
@@ -294,6 +300,74 @@ public class ConfigurationChecker implements HealthChecker {
         }
     }
 
+    static class VerifyIfDeprecatedSendMethodValuesUsed implements ConfigHealthCheck {
+        @Override
+        public List<HealthRecord> healthCheck(final Configuration config, final Locale locale) {
+            final Set<MessageSendMethod> deprecatedMethods = Arrays
+                    .stream(MessageSendMethod.values())
+                    .filter(MessageSendMethod::isDeprecated)
+                    .collect(Collectors.toSet());
+
+            final List<HealthRecord> records = new ArrayList<>();
+
+            {
+                final MessageSendMethod method = config.readSettingAsEnum(PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class);
+                if (deprecatedMethods.contains(method)) {
+                    records.add(HealthRecord.forMessage(HealthMessage.Config_InvalidSendMethod,
+                            method.toString(),
+                            PwmSetting.ACTIVATE_TOKEN_SEND_METHOD.toMenuLocationDebug(null, locale)
+                    ));
+                }
+            }
+
+            {
+                final MessageSendMethod method = config.readSettingAsEnum(PwmSetting.FORGOTTEN_USERNAME_SEND_USERNAME_METHOD, MessageSendMethod.class);
+                if (deprecatedMethods.contains(method)) {
+                    records.add(HealthRecord.forMessage(HealthMessage.Config_InvalidSendMethod,
+                            method.toString(),
+                            PwmSetting.FORGOTTEN_USERNAME_SEND_USERNAME_METHOD.toMenuLocationDebug(null, locale)
+                    ));
+                }
+            }
+
+            for (final HelpdeskProfile helpdeskProfile : config.getHelpdeskProfiles().values()) {
+                final MessageSendMethod method = helpdeskProfile.readSettingAsEnum(PwmSetting.HELPDESK_TOKEN_SEND_METHOD, MessageSendMethod.class);
+
+                if (deprecatedMethods.contains(method)) {
+                    records.add(HealthRecord.forMessage(HealthMessage.Config_InvalidSendMethod,
+                            method.toString(),
+                            PwmSetting.HELPDESK_TOKEN_SEND_METHOD.toMenuLocationDebug(helpdeskProfile.getIdentifier(), locale)
+                    ));
+                }
+            }
+
+            for (final ForgottenPasswordProfile forgottenPasswordProfile : config.getForgottenPasswordProfiles().values()) {
+                {
+                    final MessageSendMethod method = forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_SENDNEWPW_METHOD, MessageSendMethod.class);
+
+                    if (deprecatedMethods.contains(method)) {
+                        records.add(HealthRecord.forMessage(HealthMessage.Config_InvalidSendMethod,
+                                method.toString(),
+                                PwmSetting.RECOVERY_SENDNEWPW_METHOD.toMenuLocationDebug(forgottenPasswordProfile.getIdentifier(), locale)
+                        ));
+                    }
+                }
+                {
+                    final MessageSendMethod method = forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class);
+
+                    if (deprecatedMethods.contains(method)) {
+                        records.add(HealthRecord.forMessage(HealthMessage.Config_InvalidSendMethod,
+                                method.toString(),
+                                PwmSetting.RECOVERY_TOKEN_SEND_METHOD.toMenuLocationDebug(forgottenPasswordProfile.getIdentifier(), locale)
+                        ));
+                    }
+                }
+            }
+
+            return records;
+        }
+    }
+
     interface ConfigHealthCheck {
         List<HealthRecord> healthCheck(
                  Configuration configuration,

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

@@ -67,6 +67,7 @@ public enum HealthMessage {
     Config_DNValueValidity                  (HealthStatus.CONFIG,   HealthTopic.Configuration),
     Config_NoRecoveryEnabled                (HealthStatus.CAUTION,  HealthTopic.Configuration),
     Config_Certificate                      (HealthStatus.WARN,     HealthTopic.Configuration),
+    Config_InvalidSendMethod                (HealthStatus.CAUTION,  HealthTopic.Configuration),
     LDAP_VendorsNotSame                     (HealthStatus.CONFIG,   HealthTopic.LDAP),
     LDAP_OK                                 (HealthStatus.GOOD,     HealthTopic.LDAP),
     LDAP_RecentlyUnreachable                (HealthStatus.CAUTION,  HealthTopic.LDAP),

+ 7 - 163
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -24,6 +24,9 @@ package password.pwm.http.bean;
 
 import com.google.gson.annotations.SerializedName;
 import com.novell.ldapchai.cr.ChallengeSet;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Value;
 import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.value.data.FormConfiguration;
@@ -41,6 +44,7 @@ import java.util.Set;
 /**
  * @author Jason D. Rivard
  */
+@Data
 public class ForgottenPasswordBean extends PwmSessionBean {
 
     @SerializedName("u")
@@ -64,65 +68,7 @@ public class ForgottenPasswordBean extends PwmSessionBean {
     @SerializedName("fp")
     private String forgottenPasswordProfileID;
 
-
-    public UserIdentity getUserIdentity() {
-        return userIdentity;
-    }
-
-    public void setUserIdentity(final UserIdentity userIdentity) {
-        this.userIdentity = userIdentity;
-    }
-
-    public Locale getUserLocale()
-    {
-        return userLocale;
-    }
-
-    public void setUserLocale(final Locale userLocale)
-    {
-        this.userLocale = userLocale;
-    }
-
-    public Progress getProgress()
-    {
-        return progress;
-    }
-
-    public ChallengeSet getPresentableChallengeSet()
-    {
-        return presentableChallengeSet;
-    }
-
-    public void setPresentableChallengeSet(final ChallengeSet presentableChallengeSet)
-    {
-        this.presentableChallengeSet = presentableChallengeSet;
-    }
-
-    public List<FormConfiguration> getAttributeForm()
-    {
-        return attributeForm;
-    }
-
-    public void setAttributeForm(final List<FormConfiguration> attributeForm)
-    {
-        this.attributeForm = attributeForm;
-    }
-
-    public void setProgress(final Progress progress)
-    {
-        this.progress = progress;
-    }
-
-    public RecoveryFlags getRecoveryFlags()
-    {
-        return recoveryFlags;
-    }
-
-    public void setRecoveryFlags(final RecoveryFlags recoveryFlags)
-    {
-        this.recoveryFlags = recoveryFlags;
-    }
-
+    @Data
     public static class Progress implements Serializable {
         @SerializedName("s")
         private boolean tokenSent;
@@ -144,66 +90,6 @@ public class ForgottenPasswordBean extends PwmSessionBean {
 
         private transient VerificationMethodSystem remoteRecoveryMethod;
 
-        public Set<IdentityVerificationMethod> getSatisfiedMethods() {
-            return satisfiedMethods;
-        }
-
-        public boolean isTokenSent()
-        {
-            return tokenSent;
-        }
-
-        public void setTokenSent(final boolean tokenSent)
-        {
-            this.tokenSent = tokenSent;
-        }
-
-        public boolean isAllPassed()
-        {
-            return allPassed;
-        }
-
-        public void setAllPassed(final boolean allPassed)
-        {
-            this.allPassed = allPassed;
-        }
-
-        public MessageSendMethod getTokenSendChoice()
-        {
-            return tokenSendChoice;
-        }
-
-        public void setTokenSendChoice(final MessageSendMethod tokenSendChoice)
-        {
-            this.tokenSendChoice = tokenSendChoice;
-        }
-
-        public String getTokenSentAddress()
-        {
-            return tokenSentAddress;
-        }
-
-        public void setTokenSentAddress(final String tokenSentAddress)
-        {
-            this.tokenSentAddress = tokenSentAddress;
-        }
-
-        public IdentityVerificationMethod getInProgressVerificationMethod() {
-            return inProgressVerificationMethod;
-        }
-
-        public void setInProgressVerificationMethod(final IdentityVerificationMethod inProgressVerificationMethod) {
-            this.inProgressVerificationMethod = inProgressVerificationMethod;
-        }
-
-        public VerificationMethodSystem getRemoteRecoveryMethod() {
-            return remoteRecoveryMethod;
-        }
-
-        public void setRemoteRecoveryMethod(final VerificationMethodSystem remoteRecoveryMethod) {
-            this.remoteRecoveryMethod = remoteRecoveryMethod;
-        }
-
         public void clearTokenSentStatus() {
             this.setTokenSent(false);
             this.setTokenSentAddress(null);
@@ -211,6 +97,8 @@ public class ForgottenPasswordBean extends PwmSessionBean {
         }
     }
 
+    @Value
+    @AllArgsConstructor
     public static class RecoveryFlags implements Serializable {
         @SerializedName("a")
         private final boolean allowWhenLdapIntruderLocked;
@@ -235,50 +123,6 @@ public class ForgottenPasswordBean extends PwmSessionBean {
             this.minimumOptionalAuthMethods = 0;
             this.tokenSendMethod = MessageSendMethod.NONE;
         }
-
-        public RecoveryFlags(
-                final Set<IdentityVerificationMethod> requiredAuthMethods,
-                final Set<IdentityVerificationMethod> optionalAuthMethods,
-                final int minimumOptionalAuthMethods,
-                final boolean allowWhenLdapIntruderLocked,
-                final MessageSendMethod tokenSendMethod
-        )
-        {
-            this.requiredAuthMethods = Collections.unmodifiableSet(requiredAuthMethods);
-            this.optionalAuthMethods = Collections.unmodifiableSet(optionalAuthMethods);
-            this.minimumOptionalAuthMethods = minimumOptionalAuthMethods;
-            this.allowWhenLdapIntruderLocked = allowWhenLdapIntruderLocked;
-            this.tokenSendMethod = tokenSendMethod;
-        }
-
-        public Set<IdentityVerificationMethod> getRequiredAuthMethods() {
-            return requiredAuthMethods;
-        }
-
-        public boolean isAllowWhenLdapIntruderLocked()
-        {
-            return allowWhenLdapIntruderLocked;
-        }
-
-        public MessageSendMethod getTokenSendMethod() {
-            return tokenSendMethod;
-        }
-
-        public Set<IdentityVerificationMethod> getOptionalAuthMethods() {
-            return optionalAuthMethods;
-        }
-
-        public int getMinimumOptionalAuthMethods() {
-            return minimumOptionalAuthMethods;
-        }
-    }
-
-    public String getForgottenPasswordProfileID() {
-        return forgottenPasswordProfileID;
-    }
-
-    public void setForgottenPasswordProfileID(final String forgottenPasswordProfileID) {
-        this.forgottenPasswordProfileID = forgottenPasswordProfileID;
     }
 
     public Type getType() {

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

@@ -512,22 +512,9 @@ public class ActivateUserServlet extends AbstractPwmServlet {
         final Configuration config = pwmApplication.getConfig();
         final UserInfo userInfo = pwmSession.getUserInfo();
         final MessageSendMethod pref = MessageSendMethod.valueOf(config.readSettingAsString(PwmSetting.ACTIVATE_TOKEN_SEND_METHOD));
+
         final boolean success;
         switch (pref) {
-            case BOTH:
-                // Send both email and SMS, success if one of both succeeds
-                final boolean suc1 = sendPostActivationEmail(pwmRequest);
-                final boolean suc2 = sendPostActivationSms(pwmRequest);
-                success = suc1 || suc2;
-                break;
-            case EMAILFIRST:
-                // Send email first, try SMS if email is not available
-                success = sendPostActivationEmail(pwmRequest) || sendPostActivationSms(pwmRequest);
-                break;
-            case SMSFIRST:
-                // Send SMS first, try email if SMS is not available
-                success = sendPostActivationSms(pwmRequest) || sendPostActivationEmail(pwmRequest);
-                break;
             case SMSONLY:
                 // Only try SMS
                 success = sendPostActivationSms(pwmRequest);

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

@@ -276,30 +276,6 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet {
             case NONE:
                 break;
 
-            case BOTH:
-                // Send both email and SMS, success if one of both succeeds
-                final ErrorInformation err1 = sendEmailViaMethod(pwmApplication, sessionLabel, userInfo, emailItemBean);
-                final ErrorInformation err2 = sendSmsViaMethod(pwmApplication, sessionLabel, userInfo, smsMessage);
-                if (err1 != null) {
-                    error = err1;
-                } else if (err2 != null) {
-                    error = err2;
-                }
-                break;
-            case EMAILFIRST:
-                // Send email first, try SMS if email is not available
-                error = sendEmailViaMethod(pwmApplication, sessionLabel, userInfo, emailItemBean);
-                if (error != null) {
-                    error = sendSmsViaMethod(pwmApplication, sessionLabel, userInfo, smsMessage);
-                }
-                break;
-            case SMSFIRST:
-                // Send SMS first, try email if SMS is not available
-                error = sendSmsViaMethod(pwmApplication, sessionLabel, userInfo, smsMessage);
-                if (error != null) {
-                    error = sendEmailViaMethod(pwmApplication, sessionLabel, userInfo, emailItemBean);
-                }
-                break;
             case SMSONLY:
                 // Only try SMS
                 error = sendSmsViaMethod(pwmApplication, sessionLabel, userInfo, smsMessage);

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

@@ -22,11 +22,14 @@
 
 package password.pwm.http.servlet.configeditor;
 
+import lombok.Data;
 import password.pwm.config.PwmSettingCategory;
+import password.pwm.util.macro.MacroMachine;
 
 import java.io.Serializable;
 import java.util.Locale;
 
+@Data
 public class CategoryInfo implements Serializable {
     private int level;
     private String key;
@@ -37,43 +40,16 @@ public class CategoryInfo implements Serializable {
     private boolean profiles;
     private String menuLocation;
 
-    public int getLevel() {
-        return level;
-    }
-
-    public String getKey() {
-        return key;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public String getLabel() {
-        return label;
-    }
-
-    public String getParent() {
-        return parent;
-    }
-
-    public boolean isHidden() {
-        return hidden;
-    }
-
-    public boolean isProfiles() {
-        return profiles;
-    }
-
-    public String getMenuLocation() {
-        return menuLocation;
-    }
 
-    public static CategoryInfo forCategory(final PwmSettingCategory category, final Locale locale) {
+    public static CategoryInfo forCategory(
+            final PwmSettingCategory category,
+            final MacroMachine macroMachine,
+            final Locale locale)
+    {
         final CategoryInfo categoryInfo = new CategoryInfo();
         categoryInfo.key = category.getKey();
         categoryInfo.level = category.getLevel();
-        categoryInfo.description = category.getDescription(locale);
+        categoryInfo.description = macroMachine.expandMacros(category.getDescription(locale));
         categoryInfo.label = category.getLabel(locale);
         categoryInfo.hidden = category.isHidden();
         if (category.getParent() != null) {

+ 143 - 208
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -58,19 +58,19 @@ import password.pwm.health.HealthTopic;
 import password.pwm.health.LDAPStatusChecker;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.ConfigManagerBean;
 import password.pwm.http.servlet.AbstractPwmServlet;
+import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerServlet;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
 import password.pwm.util.PasswordData;
-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.java.TimeDuration;
@@ -113,7 +113,7 @@ import java.util.concurrent.ConcurrentHashMap;
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/ConfigEditor/*",
         }
 )
-public class ConfigEditorServlet extends AbstractPwmServlet {
+public class ConfigEditorServlet extends ControlledPwmServlet {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(ConfigEditorServlet.class);
 
@@ -151,123 +151,39 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         }
     }
 
-    protected ConfigEditorAction readProcessAction(final PwmRequest request)
-            throws PwmUnrecoverableException {
-        try {
-            return ConfigEditorAction.valueOf(request.readParameterAsString(PwmConstants.PARAM_ACTION_REQUEST));
-        } catch (IllegalArgumentException e) {
-            return null;
-        }
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass() {
+        return ConfigEditorAction.class;
     }
 
-
-    protected void processAction(final PwmRequest pwmRequest)
-            throws ServletException, IOException, ChaiUnavailableException, PwmUnrecoverableException
-    {
-        final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, ConfigManagerBean.class);
+    @Override
+    public ProcessStatus preProcessCheck(final PwmRequest pwmRequest) throws PwmUnrecoverableException, IOException, ServletException {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
 
         if (configManagerBean.getStoredConfiguration() == null) {
             final StoredConfigurationImpl loadedConfig = ConfigManagerServlet.readCurrentConfiguration(pwmRequest);
             configManagerBean.setConfiguration(loadedConfig);
         }
 
-        final ConfigEditorAction action = readProcessAction(pwmRequest);
-
-        if (action != null) {
-            Validator.validatePwmFormID(pwmRequest);
-
-            switch (action) {
-                case readSetting:
-                    restReadSetting(pwmRequest, configManagerBean);
-                    return;
-
-                case writeSetting:
-                    restWriteSetting(pwmRequest, configManagerBean);
-                    return;
-
-                case resetSetting:
-                    restResetSetting(pwmRequest, configManagerBean);
-                    return;
-
-                case ldapHealthCheck:
-                    restLdapHealthCheck(pwmRequest, configManagerBean);
-                    return;
-
-                case databaseHealthCheck:
-                    restDatabaseHealthCheck(pwmRequest, configManagerBean);
-                    return;
-
-                case smsHealthCheck:
-                    restSmsHealthCheck(pwmRequest, configManagerBean);
-                    return;
-
-                case finishEditing:
-                    restFinishEditing(pwmRequest, configManagerBean);
-                    return;
-
-                case executeSettingFunction:
-                    restExecuteSettingFunction(pwmRequest, configManagerBean);
-                    return;
-
-                case setConfigurationPassword:
-                    restSetConfigurationPassword(pwmRequest, configManagerBean);
-                    return;
-
-                case readChangeLog:
-                    restReadChangeLog(pwmRequest, configManagerBean);
-                    return;
-
-                case search:
-                    restSearchSettings(pwmRequest, configManagerBean);
-                    return;
-
-                case cancelEditing:
-                    restCancelEditing(pwmRequest, configManagerBean);
-                    return;
-
-                case uploadFile:
-                    doUploadFile(pwmRequest, configManagerBean);
-                    return;
-
-                case setOption:
-                    setOptions(pwmRequest, configManagerBean);
-                    return;
-
-                case menuTreeData:
-                    restMenuTreeData(pwmRequest, configManagerBean);
-                    return;
-
-                case settingData:
-                    restConfigSettingData(pwmRequest, configManagerBean);
-                    return;
-
-                case testMacro:
-                    restTestMacro(pwmRequest);
-                    return;
-
-                case browseLdap:
-                    restBrowseLdap(pwmRequest, configManagerBean);
-                    return;
-
-                case copyProfile:
-                    restCopyProfile(pwmRequest, configManagerBean);
-                    return;
+        return ProcessStatus.Continue;
+    }
 
-                default:
-                    JavaHelper.unhandledSwitchStatement(action);
-            }
-        }
+    @Override
+    protected void nextStep(final PwmRequest pwmRequest) throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException {
+        pwmRequest.forwardToJsp(JspUrl.CONFIG_MANAGER_EDITOR);
+    }
 
-        if (!pwmRequest.getPwmResponse().isCommitted()) {
-            pwmRequest.forwardToJsp(JspUrl.CONFIG_MANAGER_EDITOR);
-        }
+    private ConfigManagerBean getBean(final PwmRequest pwmRequest) throws PwmUnrecoverableException {
+        return pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, ConfigManagerBean.class);
     }
 
-    private void restExecuteSettingFunction(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "executeSettingFunction")
+    private ProcessStatus restExecuteSettingFunction(
+            final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final Map<String, String> requestMap = JsonUtil.deserializeStringMap(bodyString);
         final PwmSetting pwmSetting = PwmSetting.forKey(requestMap.get("setting"));
@@ -291,14 +207,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             }
             pwmRequest.outputJsonResult(restResultBean);
         }
+
+        return ProcessStatus.Halt;
     }
 
-    private void restReadSetting(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "readSetting")
+    private ProcessStatus restReadSetting(
+            final PwmRequest pwmRequest
     )
             throws IOException, PwmUnrecoverableException
     {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
 
         final String key = pwmRequest.readParameterAsString("key");
@@ -338,7 +257,7 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             final String errorStr = "readSettingAsString request for unknown key: " + key;
             LOGGER.warn(errorStr);
             pwmRequest.outputJsonResult(RestResultBean.fromError(new ErrorInformation(PwmError.ERROR_UNKNOWN, errorStr)));
-            return;
+            return ProcessStatus.Halt;
         } else {
             final String profile = theSetting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString("profile") : null;
             switch (theSetting.getSyntax()) {
@@ -392,13 +311,16 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         }
         returnMap.put("value", returnValue);
         pwmRequest.outputJsonResult(RestResultBean.withData(returnMap));
+        return ProcessStatus.Halt;
     }
 
-    private void restWriteSetting(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "writeSetting")
+    private ProcessStatus restWriteSetting(
+            final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
         final String key = pwmRequest.readParameterAsString("key");
         final String bodyString = pwmRequest.readRequestBodyAsString();
@@ -439,13 +361,16 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             returnMap.put("isDefault", storedConfig.isDefaultValue(setting, profileID));
         }
         pwmRequest.outputJsonResult(RestResultBean.withData(returnMap));
+        return ProcessStatus.Halt;
     }
 
-    private void restResetSetting(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "resetSetting")
+    private ProcessStatus restResetSetting(
+            final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
         final UserIdentity loggedInUser = pwmRequest.getUserInfoIfLoggedIn();
         final String key = pwmRequest.readParameterAsString("key");
@@ -463,14 +388,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         }
 
         pwmRequest.outputJsonResult(RestResultBean.forSuccessMessage(pwmRequest, Message.Success_Unknown));
+        return ProcessStatus.Halt;
     }
 
-
-    private void restSetConfigurationPassword(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "setConfigurationPassword")
+    private ProcessStatus restSetConfigurationPassword(
+            final PwmRequest pwmRequest
     )
-            throws IOException, ServletException, PwmUnrecoverableException {
+            throws IOException, ServletException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
+
         try {
             final Map<String, String> postData = pwmRequest.readBodyAsJsonStringMap();
             final String password = postData.get("password");
@@ -484,20 +412,23 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             final RestResultBean restResultBean = RestResultBean.fromError(e.getErrorInformation(), pwmRequest);
             pwmRequest.outputJsonResult(restResultBean);
         }
+
+        return ProcessStatus.Halt;
     }
 
-    private void restFinishEditing(final PwmRequest pwmRequest, final ConfigManagerBean configManagerBean)
-            throws IOException, ServletException, PwmUnrecoverableException {
+    @ActionHandler(action = "finishEditing")
+    private ProcessStatus restFinishEditing(final PwmRequest pwmRequest)
+            throws IOException, ServletException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-
-
         final List<String> validationErrors = configManagerBean.getStoredConfiguration().validateValues();
         if (!validationErrors.isEmpty()) {
             final String errorString = validationErrors.get(0);
             final ErrorInformation errorInfo = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, errorString, new String[]{errorString});
             pwmRequest.outputJsonResult(RestResultBean.fromError(errorInfo, pwmRequest));
             LOGGER.error(pwmSession, errorInfo);
-            return;
+            return ProcessStatus.Halt;
         } else {
             try {
                 ConfigManagerServlet.saveConfiguration(pwmRequest, configManagerBean.getStoredConfiguration());
@@ -509,26 +440,32 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
                 final ErrorInformation errorInfo = e.getErrorInformation();
                 pwmRequest.outputJsonResult(RestResultBean.fromError(errorInfo, pwmRequest));
                 LOGGER.error(pwmSession, errorInfo);
-                return;
             }
         }
 
+        return ProcessStatus.Halt;
     }
 
-    private void restCancelEditing(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "cancelEditing")
+    private ProcessStatus restCancelEditing(
+            final PwmRequest pwmRequest
     )
-            throws IOException, ServletException, PwmUnrecoverableException {
+            throws IOException, ServletException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         configManagerBean.setConfiguration(null);
         pwmRequest.outputJsonResult(RestResultBean.forSuccessMessage(pwmRequest, Message.Success_Unknown));
+        return ProcessStatus.Halt;
     }
 
-    private void setOptions(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "setOption")
+    private ProcessStatus setOptions(
+            final PwmRequest pwmRequest
+
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         {
             final String updateDescriptionTextCmd = pwmRequest.readParameterAsString("updateNotesText");
             if (updateDescriptionTextCmd != null && "true".equalsIgnoreCase(updateDescriptionTextCmd)) {
@@ -558,13 +495,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
                 }
             }
         }
+
+        return ProcessStatus.Halt;
     }
 
-    void restReadChangeLog(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "readChangeLog")
+    private ProcessStatus restReadChangeLog(
+            final PwmRequest pwmRequest
     )
-            throws IOException {
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final Locale locale = pwmRequest.getLocale();
         final HashMap<String, Object> returnObj = new HashMap<>();
         returnObj.put("html", configManagerBean.getStoredConfiguration().changeLogAsDebugString(locale, true));
@@ -588,15 +529,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
 
         final RestResultBean restResultBean = RestResultBean.withData(returnObj);
         pwmRequest.outputJsonResult(restResultBean);
+        return ProcessStatus.Halt;
     }
 
-    void restSearchSettings(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "search")
+    private ProcessStatus restSearchSettings(
+            final PwmRequest pwmRequest
     )
             throws IOException, PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final String bodyData = pwmRequest.readRequestBodyAsString();
         final Map<String, String> valueMap = JsonUtil.deserializeStringMap(bodyData);
         final Locale locale = pwmRequest.getLocale();
@@ -639,15 +582,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         }
 
         pwmRequest.outputJsonResult(restResultBean);
+        return ProcessStatus.Halt;
     }
 
-    private void restLdapHealthCheck(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "ldapHealthCheck")
+    private ProcessStatus restLdapHealthCheck(
+            final PwmRequest pwmRequest
     )
             throws IOException, PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         LOGGER.debug(pwmRequest, "beginning restLdapHealthCheck");
         final String profileID = pwmRequest.readParameterAsString("profile");
         final Configuration config = new Configuration(configManagerBean.getStoredConfiguration());
@@ -656,14 +601,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
 
         pwmRequest.outputJsonResult(restResultBean);
         LOGGER.debug(pwmRequest, "completed restLdapHealthCheck in " + TimeDuration.fromCurrent(startTime).asCompactString());
+        return ProcessStatus.Halt;
     }
 
-    private void restDatabaseHealthCheck(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "databaseHealthCheck")
+    private ProcessStatus restDatabaseHealthCheck(
+            final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         LOGGER.debug(pwmRequest, "beginning restDatabaseHealthCheck");
         final Configuration config = new Configuration(configManagerBean.getStoredConfiguration());
         final List<HealthRecord> healthRecords = DatabaseStatusChecker.checkNewDatabaseStatus(pwmRequest.getPwmApplication(), config);
@@ -671,14 +619,17 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         final RestResultBean restResultBean = RestResultBean.withData(healthData);
         pwmRequest.outputJsonResult(restResultBean);
         LOGGER.debug(pwmRequest, "completed restDatabaseHealthCheck in " + TimeDuration.fromCurrent(startTime).asCompactString());
+        return ProcessStatus.Halt;
     }
 
-    private void restSmsHealthCheck(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "smsHealthCheck")
+    private ProcessStatus restSmsHealthCheck(
+            final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException {
+            throws IOException, PwmUnrecoverableException
+    {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         LOGGER.debug(pwmRequest, "beginning restSmsHealthCheck");
 
         final List<HealthRecord> returnRecords = new ArrayList<>();
@@ -702,14 +653,16 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         final RestResultBean restResultBean = RestResultBean.withData(healthData);
         pwmRequest.outputJsonResult(restResultBean);
         LOGGER.debug(pwmRequest, "completed restSmsHealthCheck in " + TimeDuration.fromCurrent(startTime).asCompactString());
+        return ProcessStatus.Halt;
     }
 
-    private void doUploadFile(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "uploadFile")
+    private ProcessStatus doUploadFile(
+            final PwmRequest pwmRequest
     )
             throws PwmUnrecoverableException, IOException, ServletException
     {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final String key = pwmRequest.readParameterAsString("key");
         final PwmSetting setting = PwmSetting.forKey(key);
         final int maxFileSize = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.CONFIG_MAX_JDBC_JAR_SIZE));
@@ -738,15 +691,15 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
                 );
 
                 pwmRequest.outputJsonResult(RestResultBean.forSuccessMessage(pwmRequest, Message.Success_Unknown));
-                return;
+                return ProcessStatus.Halt;
             } catch (PwmException e) {
                 LOGGER.error(pwmRequest, "error during https certificate upload: " + e.getMessage());
                 pwmRequest.respondWithError(e.getErrorInformation(),false);
-                return;
+                return ProcessStatus.Halt;
             }
         }
 
-        final FileValue fileValue = readFileUploadToSettingValue(pwmRequest, maxFileSize);
+        final FileValue fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue(pwmRequest, maxFileSize);
         if (fileValue != null) {
             final UserIdentity userIdentity = pwmRequest.isAuthenticated()
                     ? pwmRequest.getPwmSession().getUserInfo().getUserIdentity()
@@ -755,51 +708,18 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             configManagerBean.getStoredConfiguration().writeSetting(setting, fileValue, userIdentity);
             pwmRequest.outputJsonResult(RestResultBean.forSuccessMessage(pwmRequest, Message.Success_Unknown));
         }
-    }
-
-    public static FileValue readFileUploadToSettingValue(
-            final PwmRequest pwmRequest,
-            final int maxFileSize
-    )
-            throws PwmUnrecoverableException, IOException, ServletException
-    {
-
-        final Map<String, PwmRequest.FileUploadItem> fileUploads;
-        try {
-            fileUploads = pwmRequest.readFileUploads(maxFileSize, 1);
-        } catch (PwmException e) {
-            pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
-            LOGGER.error(pwmRequest, "error during file upload: " + e.getErrorInformation().toDebugStr());
-            return null;
-        } catch (Throwable e) {
-            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "error during file upload: " + e.getMessage());
-            pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation, pwmRequest));
-            LOGGER.error(pwmRequest, errorInformation);
-            return null;
-        }
-
-        if (fileUploads.containsKey(PwmConstants.PARAM_FILE_UPLOAD)) {
-            final PwmRequest.FileUploadItem uploadItem = fileUploads.get(PwmConstants.PARAM_FILE_UPLOAD);
 
-            final Map<FileValue.FileInformation, FileValue.FileContent> newFileValueMap = new LinkedHashMap<>();
-            newFileValueMap.put(new FileValue.FileInformation(uploadItem.getName(), uploadItem.getType()), new FileValue.FileContent(uploadItem.getContent()));
-
-            return new FileValue(newFileValueMap);
-        }
-
-        final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "no file found in upload");
-        pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation, pwmRequest));
-        LOGGER.error(pwmRequest, "error during file upload: " + errorInformation.toDebugStr());
-        return null;
+        return ProcessStatus.Halt;
     }
 
-    private void restMenuTreeData(
-            final PwmRequest pwmRequest,
-            final ConfigManagerBean configManagerBean
+    @ActionHandler(action = "menuTreeData")
+    private ProcessStatus restMenuTreeData(
+            final PwmRequest pwmRequest
     )
             throws IOException, PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
 
         final ArrayList<NavTreeItem> navigationData = new ArrayList<>();
         final Map<String,Object> inputParameters = pwmRequest.readBodyAsJsonMap(PwmHttpRequestWrapper.Flag.BypassValidation);
@@ -863,25 +783,31 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
 
         LOGGER.trace(pwmRequest,"completed navigation tree data request in " + TimeDuration.fromCurrent(startTime).asCompactString());
         pwmRequest.outputJsonResult(RestResultBean.withData(navigationData));
+        return ProcessStatus.Halt;
     }
 
-    private void restConfigSettingData(final PwmRequest pwmRequest, final ConfigManagerBean configManagerBean)
-            throws IOException, PwmUnrecoverableException {
+    @ActionHandler(action = "settingData")
+    private ProcessStatus restConfigSettingData(final PwmRequest pwmRequest)
+            throws IOException, PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final PwmSettingTemplateSet template = configManagerBean.getStoredConfiguration().getTemplateSet();
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final Locale locale = pwmRequest.getLocale();
+        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel());
+
         {
             final LinkedHashMap<String, Object> settingMap = new LinkedHashMap<>();
             for (final PwmSetting setting : PwmSetting.values()) {
 
-                settingMap.put(setting.getKey(), SettingInfo.forSetting(setting, template, locale));
+                settingMap.put(setting.getKey(), SettingInfo.forSetting(setting, template, macroMachine, locale));
             }
             returnMap.put("settings", settingMap);
         }
         {
             final LinkedHashMap<String, Object> categoryMap = new LinkedHashMap<>();
             for (final PwmSettingCategory category : PwmSettingCategory.values()) {
-                categoryMap.put(category.getKey(), CategoryInfo.forCategory(category, locale));
+                categoryMap.put(category.getKey(), CategoryInfo.forCategory(category, macroMachine, locale));
             }
             returnMap.put("categories", categoryMap);
         }
@@ -912,14 +838,16 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
 
         final RestResultBean restResultBean = RestResultBean.withData(returnMap);
         pwmRequest.outputJsonResult(restResultBean);
+        return ProcessStatus.Halt;
     }
 
-    private void restTestMacro(final PwmRequest pwmRequest) throws IOException, ServletException {
+    @ActionHandler(action = "testMacro")
+    private ProcessStatus restTestMacro(final PwmRequest pwmRequest) throws IOException, ServletException {
         try {
             final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap(PwmHttpRequestWrapper.Flag.BypassValidation);
             if (inputMap == null || !inputMap.containsKey("input")) {
                 pwmRequest.outputJsonResult(RestResultBean.withData("missing input"));
-                return;
+                return ProcessStatus.Halt;
             }
 
             final MacroMachine macroMachine;
@@ -935,12 +863,15 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
             LOGGER.error(pwmRequest, e.getErrorInformation());
             pwmRequest.respondWithError(e.getErrorInformation());
         }
+        return ProcessStatus.Halt;
     }
 
-    private void restBrowseLdap(final PwmRequest pwmRequest, final ConfigManagerBean configManagerBean)
+    @ActionHandler(action = "browseLdap")
+    private ProcessStatus restBrowseLdap(final PwmRequest pwmRequest)
             throws IOException, ServletException, PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap(PwmHttpRequestWrapper.Flag.BypassValidation);
         final String profile = inputMap.get("profile");
         final String dn = inputMap.containsKey("dn") ? inputMap.get("dn") : "";
@@ -962,11 +893,14 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
                 + ", result=" + JsonUtil.serialize(result));
 
         pwmRequest.outputJsonResult(RestResultBean.withData(result));
+        return ProcessStatus.Halt;
     }
 
-    private void restCopyProfile(final PwmRequest pwmRequest, final ConfigManagerBean configManagerBean)
+    @ActionHandler(action = "copyProfile")
+    private ProcessStatus restCopyProfile(final PwmRequest pwmRequest)
             throws IOException, ServletException, PwmUnrecoverableException
     {
+        final ConfigManagerBean configManagerBean = getBean(pwmRequest);
         final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap(PwmHttpRequestWrapper.Flag.BypassValidation);
 
         final String settingKey = inputMap.get("setting");
@@ -988,5 +922,6 @@ public class ConfigEditorServlet extends AbstractPwmServlet {
         } catch (PwmUnrecoverableException e) {
             pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
         }
+        return ProcessStatus.Halt;
     }
 }

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

@@ -0,0 +1,59 @@
+package password.pwm.http.servlet.configeditor;
+
+import password.pwm.PwmConstants;
+import password.pwm.config.value.FileValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ConfigEditorServletUtils {
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass(ConfigEditorServletUtils.class);
+
+
+    public static FileValue readFileUploadToSettingValue(
+            final PwmRequest pwmRequest,
+            final int maxFileSize
+    )
+            throws PwmUnrecoverableException, IOException, ServletException
+    {
+
+        final Map<String, PwmRequest.FileUploadItem> fileUploads;
+        try {
+            fileUploads = pwmRequest.readFileUploads(maxFileSize, 1);
+        } catch (PwmException e) {
+            pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
+            LOGGER.error(pwmRequest, "error during file upload: " + e.getErrorInformation().toDebugStr());
+            return null;
+        } catch (Throwable e) {
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "error during file upload: " + e.getMessage());
+            pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation, pwmRequest));
+            LOGGER.error(pwmRequest, errorInformation);
+            return null;
+        }
+
+        if (fileUploads.containsKey(PwmConstants.PARAM_FILE_UPLOAD)) {
+            final PwmRequest.FileUploadItem uploadItem = fileUploads.get(PwmConstants.PARAM_FILE_UPLOAD);
+
+            final Map<FileValue.FileInformation, FileValue.FileContent> newFileValueMap = new LinkedHashMap<>();
+            newFileValueMap.put(new FileValue.FileInformation(uploadItem.getName(), uploadItem.getType()), new FileValue.FileContent(uploadItem.getContent()));
+
+            return new FileValue(newFileValueMap);
+        }
+
+        final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "no file found in upload");
+        pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation, pwmRequest));
+        LOGGER.error(pwmRequest, "error during file upload: " + errorInformation.toDebugStr());
+        return null;
+    }
+
+}

+ 4 - 54
server/src/main/java/password/pwm/http/servlet/configeditor/SettingInfo.java

@@ -22,6 +22,7 @@
 
 package password.pwm.http.servlet.configeditor;
 
+import lombok.Data;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingFlag;
@@ -36,6 +37,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+@Data
 public class SettingInfo implements Serializable {
     private String key;
     private String label;
@@ -51,64 +53,12 @@ public class SettingInfo implements Serializable {
     private int level;
     private List<PwmSettingFlag> flags;
 
-    public String getKey() {
-        return key;
-    }
-
-    public String getLabel() {
-        return label;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public PwmSettingCategory getCategory() {
-        return category;
-    }
-
-    public PwmSettingSyntax getSyntax() {
-        return syntax;
-    }
-
-    public boolean isHidden() {
-        return hidden;
-    }
-
-    public boolean isRequired() {
-        return required;
-    }
-
-    public Map<String, String> getOptions() {
-        return options;
-    }
-
-    public Map<PwmSettingProperty, String> getProperties() {
-        return properties;
-    }
-
-    public String getPattern() {
-        return pattern;
-    }
-
-    public String getPlaceholder() {
-        return placeholder;
-    }
-
-    public int getLevel() {
-        return level;
-    }
-
-    public List<PwmSettingFlag> getFlags() {
-        return flags;
-    }
-
-    public static SettingInfo forSetting(
+    static SettingInfo forSetting(
             final PwmSetting setting,
             final PwmSettingTemplateSet template,
+            final MacroMachine macroMachine,
             final Locale locale
     ) {
-        final MacroMachine macroMachine = MacroMachine.forStatic();
         final SettingInfo settingInfo = new SettingInfo();
         settingInfo.key = setting.getKey();
         settingInfo.description = macroMachine.expandMacros(setting.getDescription(locale));

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

@@ -57,7 +57,7 @@ import password.pwm.http.PwmURL;
 import password.pwm.http.bean.ConfigGuideBean;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.ControlledPwmServlet;
-import password.pwm.http.servlet.configeditor.ConfigEditorServlet;
+import password.pwm.http.servlet.configeditor.ConfigEditorServletUtils;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapBrowser;
 import password.pwm.ldap.schema.SchemaOperationResult;
@@ -463,7 +463,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet {
         try {
             final ConfigGuideBean configGuideBean = getBean(pwmRequest);
             final int maxFileSize = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.CONFIG_MAX_JDBC_JAR_SIZE));
-            final FileValue fileValue = ConfigEditorServlet.readFileUploadToSettingValue(pwmRequest, maxFileSize);
+            final FileValue fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue(pwmRequest, maxFileSize);
             configGuideBean.setDatabaseDriver(fileValue);
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage(pwmRequest, Message.Success_Unknown);
             pwmRequest.getPwmResponse().outputJsonResult(restResultBean);

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

@@ -1295,10 +1295,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
         final boolean allowWhenLdapIntruderLocked = forgottenPasswordProfile.readSettingAsBoolean(PwmSetting.RECOVERY_ALLOW_WHEN_LOCKED);
 
         return new ForgottenPasswordBean.RecoveryFlags(
+                allowWhenLdapIntruderLocked,
                 requiredRecoveryVerificationMethods,
                 optionalRecoveryVerificationMethods,
                 minimumOptionalRecoveryAuthMethods,
-                allowWhenLdapIntruderLocked,
                 tokenSendMethod
         );
     }

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

@@ -714,12 +714,12 @@ public class HelpdeskServlet extends ControlledPwmServlet {
         final String destEmailAddress = macroMachine.expandMacros(emailItemBean.getTo());
         final StringBuilder destDisplayString = new StringBuilder();
         if (destEmailAddress != null && !destEmailAddress.isEmpty()) {
-            if (tokenSendMethod == MessageSendMethod.BOTH || tokenSendMethod == MessageSendMethod.EMAILFIRST || tokenSendMethod == MessageSendMethod.EMAILONLY) {
+            if (tokenSendMethod == MessageSendMethod.EMAILONLY) {
                 destDisplayString.append(destEmailAddress);
             }
         }
         if (userInfo.getUserSmsNumber() != null && !userInfo.getUserSmsNumber().isEmpty()) {
-            if (tokenSendMethod == MessageSendMethod.BOTH || tokenSendMethod == MessageSendMethod.SMSFIRST || tokenSendMethod == MessageSendMethod.SMSONLY) {
+            if (tokenSendMethod == MessageSendMethod.SMSONLY) {
                 if (destDisplayString.length() > 0) {
                     destDisplayString.append(", ");
                 }

+ 4 - 0
server/src/main/java/password/pwm/i18n/Admin.java

@@ -66,6 +66,10 @@ public enum Admin implements PwmDisplayBundle {
 
     ;
 
+    public static final String STATISTICS_LABEL_PREFIX = "Statistic_Label.";
+    public static final String STATISTICS_DESCRIPTION_PREFIX =  "Statistic_Description.";
+    public static final String EPS_STATISTICS_LABEL_PREFIX = "EpsStatistic_Label.";
+
     @Override
     public String getKey() {
         return this.toString();

+ 6 - 1
server/src/main/java/password/pwm/i18n/PwmSetting.java

@@ -24,9 +24,14 @@ package password.pwm.i18n;
 
 public enum PwmSetting implements PwmDisplayBundle {
 
-
     ;
 
+    public static final String SETTING_LABEL_PREFIX = "Setting_Label_";
+    public static final String SETTING_DESCRIPTION_PREFIX = "Setting_Description_";
+
+    public static final String CATEGORY_LABEL_PREFIX = "Category_Label_";
+    public static final String CATEGORY_DESCRIPTION_PREFIX = "Category_Description_";
+
     @Override
     public String getKey() {
         return this.toString();

+ 9 - 1
server/src/main/java/password/pwm/svc/event/AuditService.java

@@ -29,6 +29,7 @@ import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SessionLabel;
+import password.pwm.i18n.Display;
 import password.pwm.ldap.UserInfo;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -44,6 +45,8 @@ import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
 import password.pwm.http.PwmSession;
 import password.pwm.svc.PwmService;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -278,7 +281,9 @@ public class AuditService implements PwmService {
     }
 
     public String sizeToDebugString() {
-        return auditVault.sizeToDebugString();
+        return auditVault == null
+                ? LocaleHelper.getLocalizedMessage(PwmConstants.DEFAULT_LOCALE, Display.Value_NotApplicable, null)
+                : auditVault.sizeToDebugString();
     }
 
     public void submit(final AuditEvent auditEvent, final UserInfo userInfo, final PwmSession pwmSession)
@@ -345,6 +350,9 @@ public class AuditService implements PwmService {
                 lastError = e.getErrorInformation();
             }
         }
+
+        // update statistics
+        StatisticsManager.incrementStat(pwmApplication, Statistic.AUDIT_EVENTS);
     }
 
 

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

@@ -45,14 +45,16 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.localdb.WorkQueueProcessor;
-import password.pwm.util.secure.X509Utils;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.localdb.LocalDBStoredQueue;
+import password.pwm.util.localdb.WorkQueueProcessor;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.X509Utils;
 
 import javax.net.SocketFactory;
 import javax.net.ssl.SSLContext;
@@ -82,11 +84,13 @@ public class SyslogAuditService {
 
 
     private final Configuration configuration;
+    private final PwmApplication pwmApplication;
     private List<SyslogIF> syslogInstances = new ArrayList<>();
 
     public SyslogAuditService(final PwmApplication pwmApplication)
             throws LocalDBException
     {
+        this.pwmApplication = pwmApplication;
         this.configuration = pwmApplication.getConfig();
         this.certificates = configuration.readSettingAsCertificate(PwmSetting.AUDIT_SYSLOG_CERTIFICATES);
 
@@ -197,6 +201,7 @@ public class SyslogAuditService {
                 syslogInstance.info(auditRecord);
                 LOGGER.trace("delivered syslog audit event: " + auditRecord);
                 lastError = null;
+                StatisticsManager.incrementStat(this.pwmApplication, Statistic.SYSLOG_MESSAGES_SENT);
                 return WorkQueueProcessor.ProcessResult.SUCCESS;
             } catch (Exception e) {
                 final String errorMsg = "error while sending syslog message to remote service: " + e.getMessage();

+ 1 - 1
server/src/main/java/password/pwm/svc/stats/EpsStatistic.java

@@ -28,7 +28,7 @@ public enum EpsStatistic {
     }
 
     public String getLabel(final Locale locale) {
-        final String keyName = "Statistic_Label." + EpsStatistic.class.getSimpleName() + "_" + this.name();
+        final String keyName = Admin.EPS_STATISTICS_LABEL_PREFIX + this.name();
         return LocaleHelper.getLocalizedMessage(locale, keyName, null, Admin.class);
     }
 }

+ 5 - 7
server/src/main/java/password/pwm/svc/stats/Statistic.java

@@ -37,6 +37,7 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 
 public enum Statistic {
+    AUDIT_EVENTS                        (Type.INCREMENTOR, "AuditEvents", null),
     AUTHENTICATIONS                     (Type.INCREMENTOR, "Authentications", null),
     AUTHENTICATION_FAILURES             (Type.INCREMENTOR, "AuthenticationFailures", null),
     AUTHENTICATION_EXPIRED              (Type.INCREMENTOR, "Authentications_Expired", null),
@@ -106,6 +107,7 @@ public enum Statistic {
     INTRUDER_ATTEMPTS                   (Type.INCREMENTOR, "IntruderAttempts", null),
     FOREIGN_SESSIONS_ACCEPTED           (Type.INCREMENTOR, "ForeignSessionsAccepted", null),
     OBSOLETE_URL_REQUESTS               (Type.INCREMENTOR, "ObsoleteUrlRequests", null),
+    SYSLOG_MESSAGES_SENT                (Type.INCREMENTOR, "SyslogMessagesSent", null),
 
     AVG_PASSWORD_SYNC_TIME              (Type.AVERAGE, "AvgPasswordSyncTime", null),
     AVG_AUTHENTICATION_TIME             (Type.AVERAGE, "AvgAuthenticationTime", null),
@@ -145,11 +147,7 @@ public enum Statistic {
     }
 
     public static SortedSet<Statistic> sortedValues(final Locale locale) {
-        final Comparator<Statistic> comparator = new Comparator<Statistic>() {
-            public int compare(final Statistic o1, final Statistic o2) {
-                return o1.getLabel(locale).compareTo(o2.getLabel(locale));
-            }
-        };
+        final Comparator<Statistic> comparator = Comparator.comparing(o -> o.getLabel(locale));
         final TreeSet<Statistic> set = new TreeSet<>(comparator);
         set.addAll(Arrays.asList(values()));
         return set;
@@ -162,7 +160,7 @@ public enum Statistic {
 
     public String getLabel(final Locale locale) {
         try {
-            final String keyName = "Statistic_Label." + this.getKey();
+            final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
             return LocaleHelper.getLocalizedMessage(locale, keyName, null, Admin.class);
         } catch (MissingResourceException e) {
             return "MISSING STATISTIC LABEL for " + this.getKey();
@@ -170,7 +168,7 @@ public enum Statistic {
     }
 
     public String getDescription(final Locale locale) {
-        final String keyName = "Statistic_Description." + this.getKey();
+        final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
         try {
             return LocaleHelper.getLocalizedMessage(locale, keyName, null, Admin.class);
         } catch (Exception e) {

+ 6 - 39
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -445,9 +445,12 @@ public class TokenService implements PwmService {
             return true;
         }
 
-        if (configuration.readSettingAsBoolean(PwmSetting.ACTIVATE_USER_ENABLE) &&
-                MessageSendMethod.NONE != configuration.readSettingAsTokenSendMethod(PwmSetting.ACTIVATE_TOKEN_SEND_METHOD)) {
-            return true;
+
+        if (configuration.readSettingAsBoolean(PwmSetting.ACTIVATE_USER_ENABLE)) {
+            final MessageSendMethod activateMethod = configuration.readSettingAsEnum(PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class);
+            if (MessageSendMethod.NONE != activateMethod) {
+                return true;
+            }
         }
 
         if (configuration.readSettingAsBoolean(PwmSetting.CHALLENGE_ENABLE)) {
@@ -634,42 +637,6 @@ public class TokenService implements PwmService {
                         // should never read here
                         LOGGER.error("attempt to send token to destination type 'NONE'");
                         throw new PwmUnrecoverableException(PwmError.ERROR_UNKNOWN);
-                    case BOTH:
-                        // Send both email and SMS, success if one of both succeeds
-                        final boolean suc1 = sendEmailToken(pwmApplication, userInfo, macroMachine, configuredEmailSetting, emailAddress, tokenKey);
-                        if (suc1) {
-                            sentTypes.add(TokenDestinationItem.Type.email);
-                        }
-                        final boolean suc2 = sendSmsToken(pwmApplication, userInfo, macroMachine, smsNumber, smsMessage, tokenKey);
-                        if (suc2) {
-                            sentTypes.add(TokenDestinationItem.Type.sms);
-                        }
-                        success = suc1 || suc2;
-                        break;
-                    case EMAILFIRST:
-                        // Send email first, try SMS if email is not available
-                        final boolean emailSuccess = sendEmailToken(pwmApplication, userInfo, macroMachine, configuredEmailSetting, emailAddress, tokenKey);
-                        if (emailSuccess) {
-                            success = true;
-                            sentTypes.add(TokenDestinationItem.Type.email);
-                        } else {
-                            success = sendSmsToken(pwmApplication, userInfo, macroMachine, smsNumber, smsMessage, tokenKey);
-                            if (success) {
-                                sentTypes.add(TokenDestinationItem.Type.sms);
-                            }
-                        }
-                        break;
-                    case SMSFIRST:
-                        // Send SMS first, try email if SMS is not available
-                        final boolean smsSuccess = sendSmsToken(pwmApplication, userInfo, macroMachine, smsNumber, smsMessage, tokenKey);
-                        if (smsSuccess) {
-                            success = true;
-                            sentTypes.add(TokenDestinationItem.Type.sms);
-                        } else {
-                            success = sendEmailToken(pwmApplication, userInfo, macroMachine, configuredEmailSetting, emailAddress, tokenKey);
-                            sentTypes.add(TokenDestinationItem.Type.email);
-                        }
-                        break;
                     case SMSONLY:
                         // Only try SMS
                         success = sendSmsToken(pwmApplication, userInfo, macroMachine, smsNumber, smsMessage, tokenKey);

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

@@ -120,36 +120,8 @@ public class PasswordUtility {
         final String smsNumber = userInfo.getUserSmsNumber();
         String returnToAddress = emailAddress;
 
-        ErrorInformation error = null;
+        final ErrorInformation error;
         switch (messageSendMethod) {
-            case BOTH:
-                // Send both email and SMS, success if one of both succeeds
-                final ErrorInformation err1 = sendNewPasswordEmail(userInfo, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
-                final ErrorInformation err2 = sendNewPasswordSms(userInfo, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
-                if (err1 != null) {
-                    error = err1;
-                    returnToAddress = smsNumber;
-                } else if (err2 != null) {
-                    error = err2;
-                }
-                break;
-            case EMAILFIRST:
-                // Send email first, try SMS if email is not available
-                error = sendNewPasswordEmail(userInfo, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
-                if (error != null) {
-                    error = sendNewPasswordSms(userInfo, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
-                    returnToAddress = smsNumber;
-                }
-                break;
-            case SMSFIRST:
-                // Send SMS first, try email if SMS is not available
-                error = sendNewPasswordSms(userInfo, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
-                if (error != null) {
-                    error = sendNewPasswordEmail(userInfo, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
-                } else {
-                    returnToAddress = smsNumber;
-                }
-                break;
             case SMSONLY:
                 // Only try SMS
                 error = sendNewPasswordSms(userInfo, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);

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

@@ -92,6 +92,17 @@
             <value />
         </default>
     </setting>
+    <setting hidden="false" key="pwm.introURL" level="1">
+        <flag>Select_AllowUserInput</flag>
+        <regex>^/.+</regex>
+        <default>
+            <value>/private</value>
+        </default>
+        <options>
+            <option value="/private">/private</option>
+            <option value="/public">/public</option>
+        </options>
+    </setting>
     <setting hidden="false" key="pwmInstanceName" level="2">
         <default>
             <value />
@@ -812,7 +823,7 @@
             <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked.","bodyHtml":""}</value>
         </default>
     </setting>
-    <setting hidden="false" key="email.pwExpirationNotice" level="1">
+    <setting hidden="true" key="email.pwExpirationNotice" level="1">
         <flag>MacroSupport</flag>
         <default>
             <value>{"to":"@User:Email@","from":"Password Expiration Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Expiration Notice","bodyPlain":"Your password is about to expire.  Your password will expire in @User:DaysUntilPwExpire@ days.","bodyHtml":""}</value>
@@ -2445,11 +2456,8 @@
             <value><![CDATA[EMAILONLY]]></value>
         </default>
         <options>
-            <option value="EMAILONLY">Email Only - Send to email address</option>
-            <option value="SMSONLY">SMS Only - Send via SMS</option>
-            <option value="BOTH">Both - Send token to both email and SMS</option>
-            <option value="EMAILFIRST">Email First - Try to send token via email; if no email address is available, send via SMS</option>
-            <option value="SMSFIRST">SMS First - Try to send token via SMS; if no SMS number is available, send via email</option>
+            <option value="EMAILONLY">Email - Send to email address</option>
+            <option value="SMSONLY">SMS - Send via SMS</option>
             <option value="CHOICE_SMS_EMAIL">User Choice - If both SMS and email address is available, user decides</option>
         </options>
     </setting>
@@ -2468,11 +2476,8 @@
             <value><![CDATA[EMAILONLY]]></value>
         </default>
         <options>
-            <option value="EMAILONLY">Email Only - Send to email address</option>
-            <option value="SMSONLY">SMS Only - Send via SMS</option>
-            <option value="BOTH">Both - Send token to both email and SMS</option>
-            <option value="EMAILFIRST">Email First - Try to send token via email; if no email address is available, send via SMS</option>
-            <option value="SMSFIRST">SMS First - Try to send token via SMS; if no SMS number is available, send via email</option>
+            <option value="EMAILONLY">Email - Send to email address</option>
+            <option value="SMSONLY">SMS - Send via SMS</option>
         </options>
     </setting>
     <setting hidden="false" key="recovery.postActions" level="2">
@@ -2539,11 +2544,8 @@
         </default>
         <options>
             <option value="NONE">None - Do not send email</option>
-            <option value="EMAILONLY">Email Only - Send to email address</option>
-            <option value="SMSONLY">SMS Only - Send via SMS</option>
-            <option value="BOTH">Both - Send token to both email and SMS</option>
-            <option value="EMAILFIRST">Email First - Try to send token via email; if no email address is available, send via SMS</option>
-            <option value="SMSFIRST">SMS First - Try to send token via SMS; if no SMS number is available, send via email</option>
+            <option value="EMAILONLY">Email - Send to email address</option>
+            <option value="SMSONLY">SMS - Send via SMS</option>
         </options>
     </setting>
     <setting hidden="false" key="newUser.enable" level="1" required="true">
@@ -2850,11 +2852,8 @@
         </default>
         <options>
             <option value="NONE">None - Token verification will not be performed</option>
-            <option value="EMAILONLY">Email Only - Send to email address</option>
-            <option value="SMSONLY">SMS Only - Send via SMS</option>
-            <option value="BOTH">Both - Send token to both email and SMS</option>
-            <option value="EMAILFIRST">Email First - Try to send token via email; if no email address is available, send via SMS</option>
-            <option value="SMSFIRST">SMS First - Try to send token via SMS; if no SMS number is available, send via email</option>
+            <option value="EMAILONLY">Email - Send to email address</option>
+            <option value="SMSONLY">SMS - Send via SMS</option>
         </options>
     </setting>
     <setting hidden="false" key="updateAttributes.enable" level="1" required="true">
@@ -3473,8 +3472,8 @@
         </default>
         <options>
             <option value="NONE">None - Token verification will not be available</option>
-            <option value="EMAILONLY">Email Only - Send to email address</option>
-            <option value="SMSONLY">SMS Only - Send via SMS</option>
+            <option value="EMAILONLY">Email - Send to email address</option>
+            <option value="SMSONLY">SMS - Send via SMS</option>
             <option value="CHOICE_SMS_EMAIL">Operator Choice - If both SMS and email address is available, helpdesk operator decides</option>
         </options>
     </setting>

+ 13 - 9
server/src/main/resources/password/pwm/i18n/Admin.properties

@@ -126,6 +126,8 @@ Confirm_Report_Clear=Do you wish clear the cached report data?  <br/><br/>This w
 Display_Report_Action_Start=Sending request to start report engine.
 Display_Report_Action_Stop=Sending request to stop the report engine.
 Display_Report_Action_Clear=Sending request to clear the cached report data.
+Statistic_Label.AuditEvents=Audit Events
+Statistic_Description.AuditEvents=Number of audit events generated and stored locally.
 Statistic_Label.Authentications=Authentications
 Statistic_Description.Authentications=Number of successful user authentications that have occurred.
 Statistic_Label.AuthenticationFailures=Authentication Failures
@@ -272,15 +274,17 @@ Statistic_Label.ForeignSessionsAccepted=Foreign Sessions Accepted
 Statistic_Description.ForeignSessionsAccepted=Number of sessions generated on a foreign or server or previous instance of this server and accepted as valid authentication.
 Statistic_Label.ObsoleteUrlRequests=Obsolete URL Requests
 Statistic_Description.ObsoleteUrlRequests=Number of web requests to obsolete URLs.
-Statistic_Label.EpsStatistic_REQUESTS=Requests
-Statistic_Label.EpsStatistic_SESSIONS=Sessions
-Statistic_Label.EpsStatistic_PASSWORD_CHANGES=Password Changes
-Statistic_Label.EpsStatistic_AUTHENTICATION=Authentications
-Statistic_Label.EpsStatistic_INTRUDER_ATTEMPTS=Intruder Attempts
-Statistic_Label.EpsStatistic_PWMDB_READS=LocalDB Reads
-Statistic_Label.EpsStatistic_PWMDB_WRITES=LocalDB Writes
-Statistic_Label.EpsStatistic_DB_READS=Database Reads
-Statistic_Label.EpsStatistic_DB_WRITES=Database Writes
+Statistic_Label.SyslogMessagesSent=Syslog Messages Sent
+Statistic_Description.SyslogMessagesSent=Number of successfully sent syslog messages.
+EpsStatistic_Label.REQUESTS=Requests
+EpsStatistic_Label.SESSIONS=Sessions
+EpsStatistic_Label.PASSWORD_CHANGES=Password Changes
+EpsStatistic_Label.AUTHENTICATION=Authentications
+EpsStatistic_Label.INTRUDER_ATTEMPTS=Intruder Attempts
+EpsStatistic_Label.PWMDB_READS=LocalDB Reads
+EpsStatistic_Label.PWMDB_WRITES=LocalDB Writes
+EpsStatistic_Label.DB_READS=Database Reads
+EpsStatistic_Label.DB_WRITES=Database Writes
 Title_About=About
 Title_DirectoryReporting=Directory Reporting
 Title_DataViewer=Data Viewer

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Health.properties

@@ -57,6 +57,7 @@ HealthMessage_Config_PasswordPolicyProblem=Password policy '%1%' configuration a
 HealthMessage_Config_UserPermissionValidity=User Permission configuration for setting %1% issue: %2%.  This may cause unexpected issues.
 HealthMessage_Config_DNValueValidity=LDAP DN configuration setting %1% issue: %2%.  This may cause unexpected issues.
 HealthMessage_Config_Certificate=Certificate for setting %1% issue: %2%
+HealthMessage_Config_InvalidSendMethod=The send method '%1%' is no longer available for setting %2%.  Please modify the configuration to use an alternate value.
 HealthMessage_LDAP_VendorsNotSame=LDAP directories of different vendor types are in use.  This configuration may cause undesirable side effects and is not supported.  %1%
 HealthMessage_LDAP_Ad_History_Asn_Missing=%1% is enabled, but the server at %2% does not support this feature.  Check to be sure it is upgraded to Windows Server 2008 R2 SP1 or greater.  Password changes against this server may fail until this is resolved.
 HealthMessage_LDAP_RecentlyUnreachable=LDAP profile %1% was recently unavailable (%2% ago at %3%): %4%

+ 5 - 10
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -191,7 +191,6 @@ Category_Label_REST_SERVER=REST Services
 Category_Label_SECURITY=Security
 Category_Label_SETTINGS=Settings
 Category_Label_SHORTCUT=Shortcut Menu
-Category_Label_PROFILE=Link Menu
 Category_Label_SMS_GATEWAY=SMS Gateway
 Category_Label_SMS_MESSAGES=SMS Messages
 Category_Label_SMS=SMS
@@ -412,8 +411,7 @@ Setting_Description_intruder.enable=Enable this option to enable the @PwmAppName
 Setting_Description_intruder.session.maxAttempts=Specify the maximum amount of intruder attempts per session.  When the user exceeds this limit, @PwmAppName@ "locks" the session, and no other requests using that session succeed.  A value of zero disables the session lockout functionality.
 Setting_Description_intruder.storageMethod=Select the data store used for Intruder Records.  If you use <b>Database</b>, all application instances share a common view of intruder status.  If you use <b>LocalDB</b>, each instance has its own intruder state.  LocalDB is likely to have less performance overhead and having a consistent intruder state across all application instances might not be important.  The Configure Guide uses a database if configured, if not it uses the LocalDB.
 Setting_Description_intruder.tokenDest.checkTime=Specify the maximum time period between each intruder attempt.  When the user exceeds this time period, @PwmAppName@ resets the intruder attempt count to zero.
-Setting_Description_intruder.tokenDest.maxAttempts=Specify the maximum number of attempts a user might make before a lockout occurs.  
-After the user exceeds this value, the user cannot perform any activities until the reset time interval has passed.   A value of zero disables the user lockout functionality.
+Setting_Description_intruder.tokenDest.maxAttempts=Specify the maximum number of attempts a token destination can be used before a lockout occurs and the token destination can no longer be sent tokens.  After the user exceeds this value, the user cannot perform any activities until the reset time interval has passed.   A value of zero disables the token lockout functionality.
 Setting_Description_intruder.tokenDest.resetTime=Specify the time period after which @PwmAppName@ clears a bad attempt from the lockout table.  @PwmAppName@ marks the attribute lockout table for a user anytime it sends a token, and it clears the lockout when the user consumes a token.  <br/><br/>Value is in number of seconds.  A value of zero disables the attribute lockout functionality.
 Setting_Description_intruder.user.checkTime=Specify the maximum time period between each intruder attempt.  When the user exceeds this time period, @PwmAppName@ resets the intruder attempt count to zero.
 Setting_Description_intruder.user.maxAttempts=Specify the maximum number of attempts a user might make before a lockout occurs.  After the user exceeds this value, the user cannot perform any activities until the reset time interval has passed.   A value of zero disables the user lockout functionality.
@@ -429,7 +427,6 @@ Setting_Description_ldap.duplicateMode=Select how @PwmAppName@ handles the situa
 Setting_Description_ldap.edirectory.cr.applyWordlist=Enable this option to prohibit users from using answers in the word list dictionary in answers when the users save the Challenge/Response answers.
 Setting_Description_ldap.edirectory.cr.maxQuestionCharsInAnswer=Specify the maximum number of characters of the question text @PwmAppName@ permits in answers when saving the Challenge/Response answers.
 Setting_Description_ldap.edirectory.cr.minRandomDuringSetup=Specify the number of random questions you require the users to complete at the time of saving their Challenge/Response answers.
-Setting_Description_ldap.edirectory.enableNmas=When connecting to a NetIQ eDirectory LDAP directory, this parameter controls if @PwmAppName@ uses NMAS extensions when connecting to the LDAP directory.  Enabling NMAS results in\:<ul><li>Better error messages when using universal password policies</li><li>Better error handling during certain change password scenarios</li></ul>Unless you are using an older version of eDirectory (pre 8.8 or before), we recommend to set this to true.<br/><br/>All NMAS operations require an SSL connection to the directory.
 Setting_Description_ldap.edirectory.readChallengeSets=Enable this option to have @PwmAppName@ read and apply the challenge set configuration from eDirectory Universal Password policy to users.  If @PwmAppName@ applies an eDirectory challenge set to the user, @PwmAppName@ uses that policy, otherwise @PwmAppName@ uses the policy that is a part of this configuration.  To require only NMAS configured challenge sets, ensure that you blank out the required and forgotten questions as part of this configuration, or else @PwmAppName@ uses those in cases where you have not defined eDirectory policy.
 Setting_Description_ldap.edirectory.readUserPwd=Enable this option to have @PwmAppName@ read the user's password from eDirectory before changing it.  This prevents @PwmAppName@ from setting an extra password change to a temporary random password during the forgotten password sequence.  If the proxy user does not have rights to read the password, then @PwmAppName@ generates a temporary random password for the user anyway.
 Setting_Description_ldap.edirectory.storeNmasResponses=Enable this option to have @PwmAppName@ save user responses to the NMAS response storage container on the user.  This storage is in addition to any other configured response storage methods.
@@ -563,6 +560,7 @@ Setting_Description_peopleSearch.enable=Enable this option to enable the People
 Setting_Description_peopleSearch.enablePublic=Enable this option to allow access to the People Search module for unauthenticated users.
 Setting_Description_peopleSearch.idleTimeout=Specify the number of seconds after which an authenticated session becomes unauthenticated.   If the value is set to 0, then @PwmAppName@ uses then the system-wide idle timeout value.  If a user is using the People Search module without authenticating, then the system does not apply a timeout.
 Setting_Description_peopleSearch.maxCacheSeconds=Specify the number of seconds that @PwmAppName@ caches the results of searches and record details that it reads from eDirectory. Use this setting to control the maximum amount of time @PwmAppName@ can use cached data. Setting to zero disables the cache entirely, but this might negatively impact the scalability of the application and the LDAP directory.
+Setting_Description_peopleSearch.orgChart.assistantAttribute=Specify the attribute that contains the LDAP DN of the assistant for a user.  If this setting is blank, @PwmAppName@ will not show the assistant on the organizational chart view.
 Setting_Description_peopleSearch.orgChart.childAttribute=Specify the attribute that contains the LDAP DN of the direct reports for a user.  If this setting is blank, @PwmAppName@ does not show the organizational chart view.
 Setting_Description_peopleSearch.orgChart.parentAttribute=Specify the attribute that contains the LDAP DN of the manager.  If this setting is blank, @PwmAppName@ does not show the organizational chart view.
 Setting_Description_peopleSearch.photo.ldapAttribute=Specify the LDAP Attribute to use for a photo. Leave this option blank, if you do not want to display a photo.
@@ -579,6 +577,7 @@ Setting_Description_pwmAdmin.queryMatch=Specify the permissions @PwmAppName@ use
 Setting_Description_pwm.appProperty.overrides=(Troubleshooting only) Specify an override application properties value.  Do not use unless directed to by a support expert.
 Setting_Description_pwm.forwardURL=Specify a URL that @PwmAppName@ forwards users to after the users complete any activity which does not require a log out.<br/><br/>You can override this setting for any given user session by adding a <i>forwardURL</i> parameter to any HTTP request. If blank, the system forwards the user to the @PwmAppName@ menu.
 Setting_Description_pwm.homeURL=Specify the URL to redirect the user to upon clicking the home button. If blank, the home button returns the user to the application context URL.
+Setting_Description_pwm.introURL=URL to redirect user to upon accessing the base context of this server (<code>@PwmContextPath@</code>).  The value must start with a slash (<code>/</code>) character, and it will be prepended by the base application path.
 Setting_Description_pwmInstanceName=Specify the name of this application instance. If blank, @PwmAppName@ uses a persistent, randomly generated value. The recommended value is blank.
 Setting_Description_pwm.logoutURL=Specify the URL to redirect user to upon logout. If users access the site through a web authentication gateway, set the Logout URL to the gateway's Logout URL. If you are using a gateway and do not include the proper logout URL here, then users encounter authentication errors, intruder lockouts, and other problems. If things are working properly then the users see the gateway log out screen when logging out.<br/><br/>You can set the Logout URL to any appropriate relative or absolute URL.  At the time the user's browser requests this URL, the local session has already been invalidated.<br/><br/>You can always override this setting for any given user session by adding a <b>logoutURL</b> parameter to any HTTP request during the session.
 Setting_Description_pwm.publishStats.enable=Enable this option to periodically share anonymous statistics of @PwmAppName@. The published statistics are\:<ul><li>Version/Build Information</li><li>Cumulative Statistics</li><li>Which settings are non-default (but not the actual setting values)</li><li>Operating system name and version</li></ul>Enabling this setting helps @PwmAppName@ developers know which features are used most often.
@@ -780,7 +779,6 @@ Setting_Label_display.showHidePasswordFields=Enable Showing Masked Fields
 Setting_Label_display.showLoginPageOptions=Show Login Page Options
 Setting_Label_display.showSuccessPage=Show Success Pages
 Setting_Label_display.updateAttributes.agreement=Update Profile Agreement Message
-Setting_Label_display.updateAttributes.preferredlanguage=Select your preferred language
 Setting_Label_email.activation=Activation Email
 Setting_Label_email.activation.token=Activation Verification Email
 Setting_Label_email.adminAlert.toAddress=System Audit Event Email Alerts
@@ -879,8 +877,6 @@ Setting_Label_helpdesk.verificationMethods=Verification Methods
 Setting_Label_helpdesk.viewStatusValues=Viewable Status Fields
 Setting_Label_http.proxy.url=HTTP Proxy
 Setting_Label_https.server.cert=HTTPS Private Key & Certificate
-Setting_Label_https.server.cert.password=HTTPS Certificate Password
-Setting_Label_https.server.cert.pkcs12=HTTPS Certificate PKCS12
 Setting_Label_https.server.tls.ciphers=TLS Ciphers
 Setting_Label_https.server.tls.protocols=TLS Protocols
 Setting_Label_idleTimeoutSeconds=Idle Timeout Seconds
@@ -911,7 +907,6 @@ Setting_Label_ldap.duplicateMode=LDAP Duplicate Mode
 Setting_Label_ldap.edirectory.cr.applyWordlist=eDirectory Challenge Set Apply Word List
 Setting_Label_ldap.edirectory.cr.maxQuestionCharsInAnswer=eDirectory Challenge Set Maximum Question Characters In Answer
 Setting_Label_ldap.edirectory.cr.minRandomDuringSetup=eDirectory Challenge Set Minimum Randoms During Setup
-Setting_Label_ldap.edirectory.enableNmas=Enable NMAS Extensions
 Setting_Label_ldap.edirectory.readChallengeSets=Read eDirectory Challenge Sets
 Setting_Label_ldap.edirectory.readUserPwd=Read User Passwords
 Setting_Label_ldap.edirectory.storeNmasResponses=Save NMAS Responses
@@ -962,7 +957,6 @@ Setting_Label_newUser.redirectUrl=After Registration Redirect URL
 Setting_Label_newUser.sms.verification=Enable New User SMS Verification
 Setting_Label_newUser.token.lifetime=New User Email Token Maximum Lifetime
 Setting_Label_newUser.token.lifetime.sms=New User SMS Token Maximum Lifetime
-Setting_Label_newUser.customLinks=Enable New User Custom links
 Setting_Label_newUser.username.definition=LDAP Entry ID Definition
 Setting_Label_newUser.writeAttributes=New User Actions
 Setting_Label_notes.noteText=Configuration Notes
@@ -1046,6 +1040,7 @@ Setting_Label_peopleSearch.enable=Enable People Search
 Setting_Label_peopleSearch.enablePublic=Enable People Search Public (Non-Authenticated) Access
 Setting_Label_peopleSearch.idleTimeout=Idle Timeout Seconds
 Setting_Label_peopleSearch.maxCacheSeconds=Search Maximum Cache Seconds
+Setting_Label_peopleSearch.orgChart.assistantAttribute=Organizational Assistant Attribute
 Setting_Label_peopleSearch.orgChart.childAttribute=Organizational Chart Child Attribute
 Setting_Label_peopleSearch.orgChart.parentAttribute=Organizational Chart Parent Attribute
 Setting_Label_peopleSearch.photo.ldapAttribute=LDAP Photo Attribute
@@ -1062,6 +1057,7 @@ Setting_Label_pwmAdmin.queryMatch=Administrator Permission
 Setting_Label_pwm.appProperty.overrides=App Property Overrides
 Setting_Label_pwm.forwardURL=Forward URL
 Setting_Label_pwm.homeURL=Home URL
+Setting_Label_pwm.introURL=Intro URL
 Setting_Label_pwmInstanceName=Instance Name
 Setting_Label_pwm.logoutURL=Logout URL
 Setting_Label_pwm.publishStats.enable=Enable Anonymous Statistics Publishing
@@ -1154,7 +1150,6 @@ Setting_Label_updateAttributes.email.verification=Enable Email Verification
 Setting_Label_updateAttributes.enable=Enable Update Profile
 Setting_Label_updateAttributes.forceSetup=Force Update Profile
 Setting_Label_updateAttributes.form=Update Profile Form
-Setting_Label_updateAttributes.preferredlanguage=Update Profile language
 Setting_Label_updateAttributes.profile.list=List of Update Attribute profiles.  In most cases, only a single profile is needed.  Only define multiple profiles if different user populations users will need different features/permissions.  Each profile has a <i>Update Attributes Profile Match</i> setting used to define to whom the profile applies.  If multiple profiles could apply for a user, the first profile in the list defined here will be assigned.
 Setting_Label_updateAttributes.queryMatch=Update Profile Match
 Setting_Label_updateAttributes.showConfirmation=Show Update Profile Confirmation

+ 2 - 2
server/src/main/webapp/index.jsp

@@ -1,4 +1,4 @@
-<%--
+<%@ page import="password.pwm.http.JspUtility" %><%--
   ~ Password Management Servlets (PWM)
   ~ http://www.pwm-project.org
   ~
@@ -28,7 +28,7 @@
 <%@ include file="WEB-INF/jsp/fragment/header.jsp" %>
 <body class="nihilo">
 <%
-    final String redirectURL = request.getContextPath() + "/private/";
+    final String redirectURL = request.getContextPath() + JspUtility.getPwmRequest(pageContext).getPwmApplication().getConfig().readSettingAsString(PwmSetting.URL_INTRO);
     response.sendRedirect(redirectURL);
 %>
 <div id="wrapper">

+ 4 - 1
server/src/main/webapp/public/reference/settings.jsp

@@ -6,6 +6,8 @@
 <%@ page import="password.pwm.util.LocaleHelper" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
 <%@ page import="java.util.*" %>
+<%@ page import="password.pwm.util.macro.MacroMachine" %>
+<%@ page import="com.novell.ldapchai.util.StringHelper" %>
 <%--
   ~ Password Management Servlets (PWM)
   ~ http://www.pwm-project.org
@@ -42,6 +44,7 @@
     final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext);
     final boolean advancedMode = false;
     final List<PwmSettingCategory> sortedCategories = PwmSettingCategory.valuesForReferenceDoc(userLocale);
+    final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel());
 %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
@@ -208,7 +211,7 @@
                 </tr>
                 <tr>
                     <td colspan="2">
-                        <%=setting.getDescription(userLocale)%>
+                        <%= macroMachine.expandMacros(setting.getDescription(userLocale)) %>
                     </td>
                 </tr>
                 <% } %>

+ 52 - 0
server/src/test/java/password/pwm/config/PwmSettingPropertyTest.java

@@ -0,0 +1,52 @@
+package password.pwm.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.PwmConstants;
+
+import java.util.HashSet;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+public class PwmSettingPropertyTest {
+
+    @Test
+    public void testForMissingSettings() {
+        final ResourceBundle resourceBundle = ResourceBundle.getBundle(password.pwm.i18n.PwmSetting.class.getName(), PwmConstants.DEFAULT_LOCALE);
+
+        final Set<String> expectedKeys = new HashSet<>();
+
+        for (final PwmSetting pwmSetting : PwmSetting.values()) {
+            final String[] keys = new String[]{
+                    password.pwm.i18n.PwmSetting.SETTING_DESCRIPTION_PREFIX + pwmSetting.getKey(),
+                    password.pwm.i18n.PwmSetting.SETTING_LABEL_PREFIX + pwmSetting.getKey(),
+            };
+            for (final String key : keys) {
+                expectedKeys.add(key);
+                Assert.assertTrue(
+                        "PwmSettings.properties missing record for " + key,
+                        resourceBundle.containsKey(key));
+            }
+        }
+
+        for (final PwmSettingCategory category : PwmSettingCategory.values()) {
+            final String[] keys = new String[]{
+                    password.pwm.i18n.PwmSetting.CATEGORY_DESCRIPTION_PREFIX + category.getKey(),
+                    password.pwm.i18n.PwmSetting.CATEGORY_LABEL_PREFIX + category.getKey(),
+            };
+            for (final String key : keys) {
+                expectedKeys.add(key);
+                Assert.assertTrue(
+                        "PwmSettings.properties missing record for " + key,
+                        resourceBundle.containsKey(key));
+            }
+        }
+
+        final Set<String> extraKeys = new HashSet<>(resourceBundle.keySet());
+        extraKeys.removeAll(expectedKeys);
+
+        if (!extraKeys.isEmpty()) {
+            Assert.fail("unexpected key in PwmSetting.properties file: " + extraKeys.iterator().next());
+        }
+    }
+}

+ 70 - 0
server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java

@@ -0,0 +1,70 @@
+package password.pwm.i18n;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.PwmConstants;
+import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.svc.stats.Statistic;
+
+import java.util.HashSet;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+public class AdminPropertyKeysTest {
+
+    @Test
+    public void testStatisticsLabelKeys() {
+        final ResourceBundle resourceBundle = ResourceBundle.getBundle(password.pwm.i18n.Admin.class.getName(), PwmConstants.DEFAULT_LOCALE);
+
+        final Set<String> expectedKeys = new HashSet<>();
+
+        for (final Statistic statistic : Statistic.values()) {
+            final String[] keys = new String[]{
+                    password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
+                    password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
+            };
+            for (final String key : keys) {
+                expectedKeys.add(key);
+                Assert.assertTrue(
+                        "Admin.properties missing record for " + key,
+                        resourceBundle.containsKey(key));
+            }
+        }
+
+        final Set<String> extraKeys = new HashSet<>(resourceBundle.keySet());
+        extraKeys.removeAll(expectedKeys);
+
+        for (final String key : extraKeys) {
+            if (key.startsWith( password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX)
+                    || key.startsWith( password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX)) {
+
+                Assert.fail("unexpected key in Admin.properties file: " + key);
+            }
+        }
+    }
+
+
+    @Test
+    public void testDpsStatisticsLabelKeys() {
+        final ResourceBundle resourceBundle = ResourceBundle.getBundle(password.pwm.i18n.Admin.class.getName(), PwmConstants.DEFAULT_LOCALE);
+
+        final Set<String> expectedKeys = new HashSet<>();
+
+        for (final EpsStatistic statistic : EpsStatistic.values()) {
+            final String key = Admin.EPS_STATISTICS_LABEL_PREFIX + statistic.name();
+            expectedKeys.add(key);
+            Assert.assertTrue(
+                    "Admin.properties missing record for " + key,
+                    resourceBundle.containsKey(key));
+        }
+
+        final Set<String> extraKeys = new HashSet<>(resourceBundle.keySet());
+        extraKeys.removeAll(expectedKeys);
+
+        for (final String key : extraKeys) {
+            if (key.startsWith( password.pwm.i18n.Admin.EPS_STATISTICS_LABEL_PREFIX)) {
+                Assert.fail("unexpected key in Admin.properties file: " + key);
+            }
+        }
+    }
+}