浏览代码

Merge branch 'master' into CEF

rkeil-git 7 年之前
父节点
当前提交
2793d9967d
共有 82 个文件被更改,包括 2160 次插入1116 次删除
  1. 20 15
      server/pom.xml
  2. 1 0
      server/src/build/checkstyle-import.xml
  3. 8 0
      server/src/main/java/password/pwm/AppProperty.java
  4. 1 2
      server/src/main/java/password/pwm/PwmApplication.java
  5. 2 2
      server/src/main/java/password/pwm/bean/TelemetryPublishBean.java
  6. 2 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  7. 9 5
      server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  8. 6 0
      server/src/main/java/password/pwm/config/option/StrengthMeterType.java
  9. 2 2
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  10. 3 2
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  11. 2 2
      server/src/main/java/password/pwm/health/ConfigurationChecker.java
  12. 29 24
      server/src/main/java/password/pwm/health/LDAPStatusChecker.java
  13. 25 10
      server/src/main/java/password/pwm/http/HttpHeader.java
  14. 7 3
      server/src/main/java/password/pwm/http/PwmRequest.java
  15. 0 1
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  16. 4 4
      server/src/main/java/password/pwm/http/SessionManager.java
  17. 19 2
      server/src/main/java/password/pwm/http/bean/DisplayElement.java
  18. 18 4
      server/src/main/java/password/pwm/http/client/PwmHttpClient.java
  19. 4 3
      server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java
  20. 13 6
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  21. 9 21
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  22. 2 2
      server/src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java
  23. 24 238
      server/src/main/java/password/pwm/http/servlet/accountinfo/AccountInformationBean.java
  24. 1 2
      server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java
  25. 4 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  26. 6 3
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  27. 14 5
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java
  28. 1 0
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  29. 4 3
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  30. 43 6
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  31. 199 65
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  32. 12 7
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  33. 20 9
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java
  34. 11 4
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  35. 34 17
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  36. 3 1
      server/src/main/java/password/pwm/i18n/Display.java
  37. 19 10
      server/src/main/java/password/pwm/ldap/LdapBrowser.java
  38. 49 18
      server/src/main/java/password/pwm/ldap/LdapConnectionService.java
  39. 10 9
      server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java
  40. 79 38
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  41. 8 4
      server/src/main/java/password/pwm/ldap/LdapPermissionTester.java
  42. 7 2
      server/src/main/java/password/pwm/ldap/UserInfo.java
  43. 6 3
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  44. 33 13
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  45. 313 0
      server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java
  46. 17 15
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  47. 1 0
      server/src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java
  48. 2 3
      server/src/main/java/password/pwm/ldap/schema/EdirSchemaExtender.java
  49. 6 5
      server/src/main/java/password/pwm/ldap/schema/SchemaManager.java
  50. 3 4
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  51. 1 0
      server/src/main/java/password/pwm/svc/PwmServiceEnum.java
  52. 0 22
      server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java
  53. 4 2
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  54. 16 0
      server/src/main/java/password/pwm/util/LocaleHelper.java
  55. 1 1
      server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java
  56. 1 1
      server/src/main/java/password/pwm/util/RandomPasswordGenerator.java
  57. 1 1
      server/src/main/java/password/pwm/util/cli/commands/ImportResponsesCommand.java
  58. 2 1
      server/src/main/java/password/pwm/util/cli/commands/LdapSchemaExtendCommand.java
  59. 2 5
      server/src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java
  60. 56 0
      server/src/main/java/password/pwm/util/localdb/LocalDBService.java
  61. 1 0
      server/src/main/java/password/pwm/util/localdb/Xodus_LocalDB.java
  62. 1 1
      server/src/main/java/password/pwm/util/logging/PwmLogEvent.java
  63. 2 2
      server/src/main/java/password/pwm/util/operations/CrService.java
  64. 56 15
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  65. 13 9
      server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  66. 1 2
      server/src/main/java/password/pwm/ws/server/RestServlet.java
  67. 5 0
      server/src/main/resources/password/pwm/AppProperty.properties
  68. 10 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  69. 3 1
      server/src/main/resources/password/pwm/i18n/Display.properties
  70. 339 0
      server/src/main/resources/password/pwm/i18n/Display_nb.properties
  71. 172 0
      server/src/main/resources/password/pwm/i18n/Error_nb.properties
  72. 173 0
      server/src/main/resources/password/pwm/i18n/Message_nb.properties
  73. 2 0
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  74. 18 1
      server/src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp
  75. 2 1
      server/src/main/webapp/WEB-INF/jsp/configguide-ldap_schema.jsp
  76. 12 14
      server/src/main/webapp/WEB-INF/jsp/error.jsp
  77. 53 374
      server/src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp
  78. 7 3
      server/src/main/webapp/public/resources/js/changepassword.js
  79. 61 51
      server/src/main/webapp/public/resources/js/main.js
  80. 7 3
      server/src/main/webapp/public/resources/js/newuser.js
  81. 4 0
      server/src/main/webapp/public/resources/style.css
  82. 19 11
      server/src/test/java/password/pwm/tests/PwmPasswordJudgeTest.java

+ 20 - 15
server/pom.xml

@@ -98,12 +98,12 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.0-RC6</version>
+                <version>3.1.0-RC8</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.0</version>
+                        <version>3.1.1</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -252,7 +252,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.2</version>
+                        <version>8.5</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -554,7 +554,7 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.0-RC6</version>
+                <version>3.1.0-RC8</version>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
@@ -575,7 +575,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.0</version>
+            <version>3.1.1</version>
             <scope>provided</scope>
         </dependency>
 
@@ -652,7 +652,7 @@
         <dependency>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
-            <version>0.6.11</version>
+            <version>0.7.0</version>
         </dependency>
         <dependency>
             <groupId>commons-net</groupId>
@@ -677,7 +677,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.6</version>
+            <version>3.7</version>
         </dependency>
         <dependency>
             <groupId>commons-validator</groupId>
@@ -692,7 +692,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.3</version>
+            <version>4.5.4</version>
         </dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
@@ -767,7 +767,7 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.0.6</version>
+            <version>1.1.0</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -777,12 +777,17 @@
         <dependency>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.34</version>
+            <version>0.35</version>
         </dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
-            <version>2.5.6</version>
+            <version>2.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.nulab-inc</groupId>
+            <artifactId>zxcvbn</artifactId>
+            <version>1.2.3</version>
         </dependency>
 
 
@@ -797,17 +802,17 @@
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dojo</artifactId>
-            <version>1.12.2</version>
+            <version>1.13.0</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dijit</artifactId>
-            <version>1.12.2</version>
+            <version>1.13.0</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dojox</artifactId>
-            <version>1.12.2</version>
+            <version>1.13.0</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
@@ -876,4 +881,4 @@
             </snapshots>
         </pluginRepository>
     </pluginRepositories>
-</project>
+</project>

+ 1 - 0
server/src/build/checkstyle-import.xml

@@ -82,6 +82,7 @@
     <allow pkg="com.github.benmanes.caffeine"/>
 
     <allow pkg="edu.umd.cs.findbugs.annotations"/>
+    <allow pkg="com.nulabinc.zxcvbn" />
 
     <subpackage name="password.pwm.util">
         <allow pkg="sun.management"/>

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

@@ -243,6 +243,14 @@ public enum AppProperty {
     PASSWORD_RANDOMGEN_MAX_ATTEMPTS                 ("password.randomGenerator.maxAttempts"),
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ("password.randomGenerator.maxLength"),
     PASSWORD_RANDOMGEN_JITTER_COUNT                 ("password.randomGenerator.jitter.count"),
+
+    /* Strength thresholds, introduced by the addition of the zxcvbn strength meter library (since it has 5 levels) */
+    PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG         ("password.strength.threshold.veryStrong"),
+    PASSWORD_STRENGTH_THRESHOLD_STRONG              ("password.strength.threshold.strong"),
+    PASSWORD_STRENGTH_THRESHOLD_GOOD                ("password.strength.threshold.good"),
+    PASSWORD_STRENGTH_THRESHOLD_WEAK                ("password.strength.threshold.weak"),
+    PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK           ("password.strength.threshold.veryWeak"),
+
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ("peoplesearch.values.verifyUserDN"),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ("peoplesearch.values.maxCount"),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ("peoplesearch.view.detail.links"),

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

@@ -22,7 +22,6 @@
 
 package password.pwm;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -445,7 +444,7 @@ public class PwmApplication {
     {
         try {
             final ChaiProvider proxiedProvider = getProxyChaiProvider(userIdentity.getLdapProfileID());
-            return ChaiFactory.createChaiUser(userIdentity.getUserDN(), proxiedProvider);
+            return proxiedProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
         } catch (ChaiUnavailableException e) {
             throw PwmUnrecoverableException.fromChaiException(e);
         }

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

@@ -22,7 +22,7 @@
 
 package password.pwm.bean;
 
-import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import lombok.Builder;
 import lombok.Getter;
 
@@ -39,7 +39,7 @@ public class TelemetryPublishBean implements Serializable {
     private final String instanceHash;
     private final String siteDescription;
     private final Instant installTime;
-    private final List<ChaiProvider.DIRECTORY_VENDOR> ldapVendor;
+    private final List<DirectoryVendor> ldapVendor;
     private final Map<String,String> statistics;
     private final List<String> configuredSettings;
     private final String versionBuild;

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

@@ -172,6 +172,8 @@ public enum PwmSetting {
             "password.showAutoGen", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.CHANGE_PASSWORD),
     PASSWORD_SHOW_STRENGTH_METER(
             "password.showStrengthMeter", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.CHANGE_PASSWORD),
+    PASSWORD_STRENGTH_METER_TYPE(
+            "password.strengthMeter.type", PwmSettingSyntax.SELECT, PwmSettingCategory.CHANGE_PASSWORD),
 
     // account info
     ACCOUNT_INFORMATION_ENABLED(

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

@@ -23,7 +23,7 @@
 package password.pwm.config.function;
 
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
@@ -127,13 +127,17 @@ public class UserMatchViewerFunction implements SettingUIFunction {
             ChaiEntry chaiEntry = null;
             try {
                 final ChaiProvider proxiedProvider = pwmApplication.getProxyChaiProvider(loopID);
-                chaiEntry = ChaiFactory.createChaiEntry(baseDN, proxiedProvider);
+                chaiEntry = proxiedProvider.getEntryFactory().newChaiEntry(baseDN);
             } catch (Exception e) {
                 LOGGER.error("error while testing entry DN for profile '" + profileID + "', error:" + profileID);
             }
-            if (chaiEntry != null && !chaiEntry.isValid()) {
-                final String errorMsg = "entry DN '" + baseDN + "' is not valid for profile " + loopID;
-                throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_LDAP_DATA_ERROR, errorMsg));
+            try {
+                if (chaiEntry != null && !chaiEntry.exists()) {
+                    final String errorMsg = "entry DN '" + baseDN + "' is not valid for profile " + loopID;
+                    throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_LDAP_DATA_ERROR, errorMsg));
+                }
+            } catch (ChaiUnavailableException e) {
+                throw PwmUnrecoverableException.fromChaiException(e);
             }
         }
     }

+ 6 - 0
server/src/main/java/password/pwm/config/option/StrengthMeterType.java

@@ -0,0 +1,6 @@
+package password.pwm.config.option;
+
+public enum StrengthMeterType implements ConfigurationOption {
+    PWM,
+    ZXCVBN,
+}

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

@@ -23,7 +23,7 @@
 package password.pwm.config.profile;
 
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
+
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -146,7 +146,7 @@ public class LdapProfile extends AbstractProfile implements Profile {
         if (canonicalValue == null) {
             try {
                 final ChaiProvider chaiProvider = this.getProxyChaiProvider(pwmApplication);
-                final ChaiEntry chaiEntry = ChaiFactory.createChaiEntry(dnValue, chaiProvider);
+                final ChaiEntry chaiEntry = chaiProvider.getEntryFactory().newChaiEntry(dnValue);
                 canonicalValue = chaiEntry.readCanonicalDN();
 
                 if (enableCanonicalCache) {

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

@@ -22,9 +22,9 @@
 
 package password.pwm.config.profile;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -115,7 +115,8 @@ public class NewUserProfile extends AbstractProfile {
                 throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,"user ldap dn in setting " + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug(null,PwmConstants.DEFAULT_LOCALE) + " can not be resolved"));
             } else {
                 try {
-                    final ChaiUser chaiUser = ChaiFactory.createChaiUser(lookupDN, pwmApplication.getProxyChaiProvider(defaultLdapProfile.getIdentifier()));
+                    final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(defaultLdapProfile.getIdentifier());
+                    final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser(lookupDN);
                     final UserIdentity userIdentity = new UserIdentity(lookupDN, defaultLdapProfile.getIdentifier());
                     thePolicy = PasswordUtility.readPasswordPolicyForUser(pwmApplication, null, userIdentity, chaiUser, userLocale);
                 } catch (ChaiUnavailableException e) {

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

@@ -162,7 +162,7 @@ public class ConfigurationChecker implements HealthChecker {
                         if (!config.isDefaultValue(setting)) {
                             try {
                                 final PasswordData passwordValue = config.readSettingAsPassword(setting);
-                                final int strength = PasswordUtility.judgePasswordStrength(
+                                final int strength = PasswordUtility.judgePasswordStrength(config,
                                         passwordValue.getStringValue());
                                 if (strength < 50) {
                                     records.add(HealthRecord.forMessage(HealthMessage.Config_WeakPassword,
@@ -180,7 +180,7 @@ public class ConfigurationChecker implements HealthChecker {
                 final PwmSetting setting = PwmSetting.LDAP_PROXY_USER_PASSWORD;
                 try {
                     final PasswordData passwordValue = profile.readSettingAsPassword(setting);
-                    final int strength = PasswordUtility.judgePasswordStrength(passwordValue == null ? null : passwordValue.getStringValue());
+                    final int strength = PasswordUtility.judgePasswordStrength(config, passwordValue == null ? null : passwordValue.getStringValue());
                     if (strength < 50) {
                         records.add(HealthRecord.forMessage(HealthMessage.Config_WeakPassword,
                                 setting.toMenuLocationDebug(profile.getIdentifier(),locale),

+ 29 - 24
server/src/main/java/password/pwm/health/LDAPStatusChecker.java

@@ -23,7 +23,6 @@
 package password.pwm.health;
 
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiError;
 import com.novell.ldapchai.exception.ChaiErrors;
@@ -31,8 +30,8 @@ import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
-import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import com.novell.ldapchai.util.ChaiUtility;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
@@ -100,7 +99,7 @@ public class LDAPStatusChecker implements HealthChecker {
                     checkBasicLdapConnectivity(pwmApplication, config, entry.getValue(), true));
 
             if (profileRecords.isEmpty()) {
-                profileRecords.addAll(checkLdapServerUrls(config, ldapProfiles.get(profileID)));
+                profileRecords.addAll(checkLdapServerUrls(pwmApplication, config, ldapProfiles.get(profileID)));
             }
 
             if (profileRecords.isEmpty()) {
@@ -191,6 +190,7 @@ public class LDAPStatusChecker implements HealthChecker {
             try {
 
                 chaiProvider = LdapOperationsHelper.createChaiProvider(
+                        pwmApplication,
                         SessionLabel.HEALTH_SESSION_LABEL,
                         ldapProfile,
                         config,
@@ -198,7 +198,7 @@ public class LDAPStatusChecker implements HealthChecker {
                         proxyUserPW
                 );
 
-                theUser = ChaiFactory.createChaiUser(testUserDN, chaiProvider);
+                theUser = chaiProvider.getEntryFactory().newChaiUser(testUserDN);
 
             } catch (ChaiUnavailableException e) {
                 returnRecords.add(HealthRecord.forMessage(HealthMessage.LDAP_TestUserUnavailable,
@@ -230,7 +230,7 @@ public class LDAPStatusChecker implements HealthChecker {
             LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "beginning process to check ldap test user password read/write operations for profile " + ldapProfile.getIdentifier());
             try {
                 final boolean readPwdEnabled = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.EDIRECTORY_READ_USER_PWD)
-                        && theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY;
+                        && theUser.getChaiProvider().getDirectoryVendor() == DirectoryVendor.EDIRECTORY;
 
                 if (readPwdEnabled) {
                     try {
@@ -352,7 +352,11 @@ public class LDAPStatusChecker implements HealthChecker {
     }
 
 
-    public List<HealthRecord> checkLdapServerUrls(final Configuration config, final LdapProfile ldapProfile)
+    public List<HealthRecord> checkLdapServerUrls(
+            final PwmApplication pwmApplication,
+            final Configuration config,
+            final LdapProfile ldapProfile
+    )
     {
         final List<HealthRecord> returnRecords = new ArrayList<>();
         final List<String> serverURLs = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_SERVER_URLS);
@@ -361,6 +365,7 @@ public class LDAPStatusChecker implements HealthChecker {
             ChaiProvider chaiProvider = null;
             try {
                 chaiProvider = LdapOperationsHelper.createChaiProvider(
+                        pwmApplication,
                         SessionLabel.HEALTH_SESSION_LABEL,
                         config,
                         ldapProfile,
@@ -368,8 +373,8 @@ public class LDAPStatusChecker implements HealthChecker {
                         proxyDN,
                         ldapProfile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD)
                 );
-                final ChaiUser proxyUser = ChaiFactory.createChaiUser(proxyDN, chaiProvider);
-                proxyUser.isValid();
+                final ChaiUser proxyUser = chaiProvider.getEntryFactory().newChaiUser(proxyDN);
+                proxyUser.exists();
             } catch (Exception e) {
                 final String errorString = "error connecting to ldap server '" + loopURL + "': " + e.getMessage();
                 returnRecords.add(new HealthRecord(
@@ -395,7 +400,7 @@ public class LDAPStatusChecker implements HealthChecker {
         final List<HealthRecord> returnRecords = new ArrayList<>();
         ChaiProvider chaiProvider = null;
         try{
-            ChaiProvider.DIRECTORY_VENDOR directoryVendor = null;
+            final DirectoryVendor directoryVendor;
             try {
                 final String proxyDN = ldapProfile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN);
                 final PasswordData proxyPW = ldapProfile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD);
@@ -405,9 +410,9 @@ public class LDAPStatusChecker implements HealthChecker {
                 if (proxyPW == null) {
                     return Collections.singletonList(new HealthRecord(HealthStatus.WARN,HealthTopic.LDAP,"Missing Proxy User Password"));
                 }
-                chaiProvider = LdapOperationsHelper.createChaiProvider(SessionLabel.HEALTH_SESSION_LABEL,ldapProfile,config,proxyDN,proxyPW);
-                final ChaiEntry adminEntry = ChaiFactory.createChaiEntry(proxyDN,chaiProvider);
-                adminEntry.isValid();
+                chaiProvider = LdapOperationsHelper.createChaiProvider(pwmApplication, SessionLabel.HEALTH_SESSION_LABEL,ldapProfile,config,proxyDN,proxyPW);
+                final ChaiEntry adminEntry = chaiProvider.getEntryFactory().newChaiEntry(proxyDN);
+                adminEntry.exists();
                 directoryVendor = chaiProvider.getDirectoryVendor();
             } catch (ChaiException e) {
                 final ChaiError chaiError = ChaiErrors.getErrorForMessage(e.getMessage());
@@ -439,14 +444,14 @@ public class LDAPStatusChecker implements HealthChecker {
                 return returnRecords;
             }
 
-            if (directoryVendor != null && directoryVendor == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+            if (directoryVendor != null && directoryVendor == DirectoryVendor.ACTIVE_DIRECTORY) {
                 returnRecords.addAll(checkAd(pwmApplication, config, ldapProfile));
             }
 
             if (testContextlessRoot) {
                 for (final String loopContext : ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_CONTEXTLESS_ROOT)) {
                     try {
-                        final ChaiEntry contextEntry = ChaiFactory.createChaiEntry(loopContext,chaiProvider);
+                        final ChaiEntry contextEntry = chaiProvider.getEntryFactory().newChaiEntry(loopContext);
                         final Set<String> objectClasses = contextEntry.readObjectClass();
 
                         if (objectClasses == null || objectClasses.isEmpty()) {
@@ -538,7 +543,7 @@ public class LDAPStatusChecker implements HealthChecker {
 
         LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL,"beginning check for replica vendor sameness");
         boolean errorReachingServer = false;
-        final Map<String,ChaiProvider.DIRECTORY_VENDOR> replicaVendorMap = new HashMap<>();
+        final Map<String, DirectoryVendor> replicaVendorMap = new HashMap<>();
 
         try {
             for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) {
@@ -548,8 +553,8 @@ public class LDAPStatusChecker implements HealthChecker {
                 );
                 final Collection<ChaiConfiguration> replicaConfigs = ChaiUtility.splitConfigurationPerReplica(profileChaiConfiguration, Collections.<ChaiSetting,String>emptyMap());
                 for (final ChaiConfiguration chaiConfiguration : replicaConfigs) {
-                    final ChaiProvider loopProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
-                    replicaVendorMap.put(chaiConfiguration.getSetting(ChaiSetting.BIND_URLS),loopProvider.getDirectoryVendor());
+                    final ChaiProvider loopProvider = pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(chaiConfiguration);
+                    replicaVendorMap.put(chaiConfiguration.getSetting(ChaiSetting.BIND_URLS), loopProvider.getDirectoryVendor());
                 }
             }
         } catch (Exception e) {
@@ -558,12 +563,12 @@ public class LDAPStatusChecker implements HealthChecker {
         }
 
         final ArrayList<HealthRecord> healthRecords = new ArrayList<>();
-        final Set<ChaiProvider.DIRECTORY_VENDOR> discoveredVendors = new HashSet<>(replicaVendorMap.values());
+        final Set<DirectoryVendor> discoveredVendors = new HashSet<>(replicaVendorMap.values());
 
         if (discoveredVendors.size() >= 2) {
             final StringBuilder vendorMsg = new StringBuilder();
-            for (final Iterator<Map.Entry<String,ChaiProvider.DIRECTORY_VENDOR>> iterator = replicaVendorMap.entrySet().iterator(); iterator.hasNext(); ) {
-                final Map.Entry<String,ChaiProvider.DIRECTORY_VENDOR> entry = iterator.next();
+            for (final Iterator<Map.Entry<String, DirectoryVendor>> iterator = replicaVendorMap.entrySet().iterator(); iterator.hasNext(); ) {
+                final Map.Entry<String, DirectoryVendor> entry = iterator.next();
                 final String key = entry.getKey();
                 vendorMsg.append(key).append("=").append(entry.getValue().toString());
                 if (iterator.hasNext()) {
@@ -613,7 +618,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 );
                 final Collection<ChaiConfiguration> replicaConfigs = ChaiUtility.splitConfigurationPerReplica(profileChaiConfiguration, Collections.<ChaiSetting,String>emptyMap());
                 for (final ChaiConfiguration chaiConfiguration : replicaConfigs) {
-                    final ChaiProvider loopProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
+                    final ChaiProvider loopProvider = pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(chaiConfiguration);
                     final ChaiEntry rootDSE = ChaiUtility.getRootDSE(loopProvider);
                     final Set<String> controls = rootDSE.readMultiStringAttribute("supportedControl");
                     final boolean asnSupported = controls.contains(PwmConstants.LDAP_AD_PASSWORD_POLICY_CONTROL_ASN);
@@ -765,8 +770,8 @@ public class LDAPStatusChecker implements HealthChecker {
         final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(ldapProfileID);
         try {
             if (!isExampleDN(dnValue)) {
-                final ChaiEntry baseDNEntry = ChaiFactory.createChaiEntry(dnValue, chaiProvider);
-                if (!baseDNEntry.isValid()) {
+                final ChaiEntry baseDNEntry = chaiProvider.getEntryFactory().newChaiEntry(dnValue);
+                if (!baseDNEntry.exists()) {
                     return "DN '" + dnValue + "' is invalid";
                 } else {
                     final String canonicalDN = baseDNEntry.readCanonicalDN();
@@ -819,7 +824,7 @@ public class LDAPStatusChecker implements HealthChecker {
         profileRecords.addAll(ldapStatusChecker.checkBasicLdapConnectivity(tempApplication, config, ldapProfile,
                 testContextless));
         if (fullTest) {
-            profileRecords.addAll(ldapStatusChecker.checkLdapServerUrls(config, ldapProfile));
+            profileRecords.addAll(ldapStatusChecker.checkLdapServerUrls(pwmApplication, config, ldapProfile));
         }
 
         if (profileRecords.isEmpty()) {

+ 25 - 10
server/src/main/java/password/pwm/http/HttpHeader.java

@@ -23,10 +23,8 @@
 package password.pwm.http;
 
 import password.pwm.PwmConstants;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 
 public enum HttpHeader {
     Accept("Accept"),
@@ -44,7 +42,7 @@ public enum HttpHeader {
     Content_Language("Content-Language"),
     Accept_Encoding("Accept-Encoding"),
     Accept_Language("Accept-Language"),
-    Authorization("Authorization", Flag.Sensitive),
+    Authorization("Authorization", Property.Sensitive),
     UserAgent("User-Agent"),
     Referer("Referer"),
     Origin("Origin"),
@@ -64,22 +62,39 @@ public enum HttpHeader {
 
     ;
 
-    enum Flag {
+    private enum Property {
         Sensitive
     }
 
-
-    private final Collection<Flag> flags;
     private final String httpName;
+    private final Property[] properties;
 
-    HttpHeader(final String httpName, final Flag... flags)
+    HttpHeader(final String httpName, final Property... properties)
     {
         this.httpName = httpName;
-        this.flags = Collections.unmodifiableList(flags == null ? Collections.emptyList() : Arrays.asList(flags));
+        this.properties = properties;
     }
 
     public String getHttpName()
     {
         return httpName;
     }
+
+    public boolean isSensitive() {
+        return JavaHelper.enumArrayContainsValue(properties, Property.Sensitive);
+    }
+
+    public static HttpHeader forHttpHeader(final String header) {
+        if (StringUtil.isEmpty(header)) {
+            return null;
+        }
+
+        for (final HttpHeader httpHeader : values()) {
+            if (header.equals(httpHeader.getHttpName())) {
+                return httpHeader;
+            }
+        }
+
+        return null;
+    }
 }

+ 7 - 3
server/src/main/java/password/pwm/http/PwmRequest.java

@@ -41,6 +41,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.command.CommandServlet;
 import password.pwm.ldap.UserInfo;
@@ -290,7 +291,10 @@ public class PwmRequest extends PwmHttpRequestWrapper {
         Validator.validatePwmFormID(this);
     }
 
-    public boolean convertURLtokenCommand()
+    public boolean convertURLtokenCommand(
+            final PwmServletDefinition pwmServletDefinition,
+            final AbstractPwmServlet.ProcessAction processAction
+    )
             throws IOException, PwmUnrecoverableException
     {
         final String uri = getURLwithoutQueryString();
@@ -324,9 +328,9 @@ public class PwmRequest extends PwmHttpRequestWrapper {
 
         final StringBuilder redirectURL = new StringBuilder();
         redirectURL.append(this.getHttpServletRequest().getContextPath());
-        redirectURL.append(this.getHttpServletRequest().getServletPath());
+        redirectURL.append(pwmServletDefinition.servletUrl());
         redirectURL.append("?");
-        redirectURL.append(PwmConstants.PARAM_ACTION_REQUEST).append("=enterCode");
+        redirectURL.append(PwmConstants.PARAM_ACTION_REQUEST).append("=").append(processAction.toString());
         redirectURL.append("&");
         redirectURL.append(PwmConstants.PARAM_TOKEN).append("=").append(tokenValue);
 

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

@@ -53,7 +53,6 @@ public enum PwmRequestAttribute {
 
     HelpdeskDetail,
     HelpdeskObfuscatedDN,
-    HelpdeskUsername,
     HelpdeskVerificationEnabled,
 
     ConfigFilename,

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

@@ -22,7 +22,6 @@
 
 package password.pwm.http;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -88,6 +87,7 @@ public class SessionManager {
 
         try {
             this.chaiProvider = LdapOperationsHelper.createChaiProvider(
+                    pwmApplication,
                     pwmSession.getLabel(),
                     userIdentity.getLdapProfile(pwmApplication.getConfig()),
                     pwmApplication.getConfig(),
@@ -95,7 +95,7 @@ public class SessionManager {
                     userPassword
             );
             final String userDN = userIdentity.getUserDN();
-            ChaiFactory.createChaiEntry(userDN,chaiProvider).isValid();
+            chaiProvider.getEntryFactory().newChaiEntry(userDN).exists();
         } catch (ChaiUnavailableException e) {
             final ErrorInformation errorInformation = new ErrorInformation(
                     PwmError.ERROR_DIRECTORY_UNAVAILABLE,
@@ -130,7 +130,7 @@ public class SessionManager {
             throw new IllegalStateException("user not logged in");
         }
 
-        return ChaiFactory.createChaiUser(userDN.getUserDN(), this.getChaiProvider());
+        return this.getChaiProvider().getEntryFactory().newChaiUser(userDN.getUserDN());
     }
 
     public boolean hasActiveLdapConnection() {
@@ -149,7 +149,7 @@ public class SessionManager {
                 throw new PwmUnrecoverableException(PwmError.ERROR_NO_LDAP_CONNECTION);
             }
             final ChaiProvider provider = this.getChaiProvider();
-            return ChaiFactory.createChaiUser(userIdentity.getUserDN(), provider);
+            return provider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
         } catch (ChaiUnavailableException e) {
             throw PwmUnrecoverableException.fromChaiException(e);
         }

+ 19 - 2
server/src/main/java/password/pwm/http/bean/DisplayElement.java

@@ -1,19 +1,36 @@
 package password.pwm.http.bean;
 
-import lombok.Value;
+import lombok.Getter;
 
 import java.io.Serializable;
+import java.util.List;
 
-@Value
+@Getter
 public class DisplayElement implements Serializable {
     private String key;
     private Type type;
     private String label;
     private String value;
+    private List<String> values;
 
     public enum Type {
         string,
         timestamp,
         number,
+        multiString,
+    }
+
+    public DisplayElement(final String key, final Type type, final String label, final String value) {
+        this.key = key;
+        this.type = type;
+        this.label = label;
+        this.value = value;
+    }
+
+    public DisplayElement(final String key, final Type type, final String label, final List<String> values) {
+        this.key = key;
+        this.type = type;
+        this.label = label;
+        this.values = values;
     }
 }

+ 18 - 4
server/src/main/java/password/pwm/http/client/PwmHttpClient.java

@@ -60,6 +60,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -178,10 +179,23 @@ public class PwmHttpClient {
             msg.append(" (no body)");
         }
         msg.append("\n");
-        for (final Map.Entry<String,String> entry : headers.entrySet()) {
-            final String key = entry.getKey();
-            final String value = entry.getValue();
-            msg.append("  header: ").append(key).append("=").append(value).append("\n");
+        for (final Map.Entry<String, String> headerEntry : headers.entrySet()) {
+            final HttpHeader httpHeader = HttpHeader.forHttpHeader(headerEntry.getKey());
+            if (httpHeader != null) {
+                final boolean sensitive = httpHeader.isSensitive();
+                msg.append("  header: ").append( httpHeader.getHttpName() ).append("=");
+
+                if (sensitive) {
+                    msg.append(PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT);
+                } else {
+                    msg.append(headerEntry.getValue());
+                }
+            } else {
+                // We encountered a header name that doesn't have a corresponding enum in HttpHeader,
+                // so we can't check the sensitive flag.
+                msg.append("  header: ").append(headerEntry.getKey()).append("=").append(headerEntry.getValue());
+            }
+            msg.append("\n");
         }
         if (body != null && !body.isEmpty()) {
             msg.append("  body: ").append(body);

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

@@ -22,11 +22,11 @@
 
 package password.pwm.http.servlet;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
+import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
@@ -171,7 +171,7 @@ public class ActivateUserServlet extends AbstractPwmServlet {
 
         // convert a url command like /pwm/public/NewUserServlet/12321321 to redirect with a process action.
         if (action == null) {
-            if (pwmRequest.convertURLtokenCommand()) {
+            if (pwmRequest.convertURLtokenCommand(PwmServletDefinition.ActivateUser, ActivateUserAction.enterCode)) {
                 return;
             }
         } else {
@@ -476,7 +476,8 @@ public class ActivateUserServlet extends AbstractPwmServlet {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final String searchFilter = figureLdapSearchFilter(pwmRequest);
-        final ChaiUser chaiUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID()));
+        final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
+        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
 
         for (final Map.Entry<FormConfiguration, String> entry : formValues.entrySet()) {
             final FormConfiguration formItem = entry.getKey();

+ 13 - 6
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -277,12 +277,16 @@ public class ClientApiServlet extends ControlledPwmServlet {
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
+        final Locale userLocale = pwmSession.getSessionStateBean().getLocale();
+
         final Configuration config = pwmApplication.getConfig();
         final TreeMap<String,Object> settingMap = new TreeMap<>();
+
         settingMap.put("client.ajaxTypingTimeout", Integer.parseInt(config.readAppProperty(AppProperty.CLIENT_AJAX_TYPING_TIMEOUT)));
         settingMap.put("client.ajaxTypingWait", Integer.parseInt(config.readAppProperty(AppProperty.CLIENT_AJAX_TYPING_WAIT)));
         settingMap.put("client.activityMaxEpsRate", Integer.parseInt(config.readAppProperty(AppProperty.CLIENT_ACTIVITY_MAX_EPS_RATE)));
         settingMap.put("client.js.enableHtml5Dialog", Boolean.parseBoolean(config.readAppProperty(AppProperty.CLIENT_JS_ENABLE_HTML5DIALOG)));
+        settingMap.put("client.locale", LocaleHelper.getBrowserLocaleString(pwmSession.getSessionStateBean().getLocale()) );
         settingMap.put("client.pwShowRevertTimeout", Integer.parseInt(config.readAppProperty(AppProperty.CLIENT_PW_SHOW_REVERT_TIMEOUT)));
         settingMap.put("enableIdleTimeout", config.readSettingAsBoolean(PwmSetting.DISPLAY_IDLE_TIMEOUT));
         settingMap.put("pageLeaveNotice", config.readSettingAsLong(PwmSetting.SECURITY_PAGE_LEAVE_NOTICE_TIMEOUT));
@@ -293,7 +297,7 @@ public class ClientApiServlet extends ControlledPwmServlet {
         {
             long idleSeconds = config.readSettingAsLong(PwmSetting.IDLE_TIMEOUT_SECONDS);
             if (pageUrl == null || pageUrl.isEmpty()) {
-                LOGGER.warn(pwmSession, "request to /client data did not incliude pageUrl");
+                LOGGER.warn(pwmSession, "request to /client data did not include pageUrl");
             } else {
                 try {
                     final PwmURL pwmURL = new PwmURL(new URI(pageUrl), request.getContextPath());
@@ -357,14 +361,17 @@ public class ClientApiServlet extends ControlledPwmServlet {
         }
 
         {
-            final Map<String,String> localeInfo = new TreeMap<>();
-            final Map<String,String> localeDisplayNames = new TreeMap<>();
-            final Map<String,String> localeFlags = new TreeMap<>();
+            final Map<String,String> localeInfo = new LinkedHashMap<>();
+            final Map<String,String> localeDisplayNames = new LinkedHashMap<>();
+            final Map<String,String> localeFlags = new LinkedHashMap<>();
+
+            final List<Locale> knownLocales = new ArrayList<>(pwmApplication.getConfig().getKnownLocales());
+            knownLocales.sort(LocaleHelper.localeComparator( PwmConstants.DEFAULT_LOCALE));
 
-            for (final Locale locale : pwmApplication.getConfig().getKnownLocales()) {
+            for (final Locale locale : knownLocales ) {
                 final String flagCode = pwmApplication.getConfig().getKnownLocaleFlagMap().get(locale);
                 localeFlags.put(locale.toString(),flagCode);
-                localeInfo.put(locale.toString(),locale.getDisplayName() + " - " + locale.getDisplayLanguage(locale));
+                localeInfo.put(locale.toString(),locale.getDisplayName( PwmConstants.DEFAULT_LOCALE) + " - " + locale.getDisplayLanguage(userLocale));
                 localeDisplayNames.put(locale.toString(),locale.getDisplayLanguage());
             }
 

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

@@ -22,7 +22,6 @@
 
 package password.pwm.http.servlet;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
@@ -231,7 +230,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
                     Collections.singletonList(guestRegistrationBean.getUpdateUserIdentity())
             );
 
-            final Date expirationDate = readExpirationFromRequest(pwmRequest);
+            final Instant expirationDate = readExpirationFromRequest(pwmRequest);
 
             // Update user attributes
             LdapOperationsHelper.writeFormValuesToLdap(pwmApplication, pwmSession.getSessionManager().getMacroMachine(pwmApplication), theGuest, formValues, false);
@@ -344,9 +343,9 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
                 }
                 final String expirationAttribute = config.readSettingAsString(PwmSetting.GUEST_EXPIRATION_ATTRIBUTE);
                 if (expirationAttribute != null && expirationAttribute.length() > 0) {
-                    final Date expiration = guestUserInfo.readDateAttribute(expirationAttribute);
+                    final Instant expiration = guestUserInfo.readDateAttribute(expirationAttribute);
                     if (expiration != null) {
-                        guBean.setUpdateUserExpirationDate(expiration.toInstant());
+                        guBean.setUpdateUserExpirationDate(expiration);
                     }
                 }
 
@@ -395,7 +394,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
                     pwmRequest, guestUserForm, locale);
 
             //read the expiration date from the request.
-            final Date expirationDate = readExpirationFromRequest(pwmRequest);
+            final Instant expirationDate = readExpirationFromRequest(pwmRequest);
 
             // see if the values meet form requirements.
             FormUtility.validateFormValues(config, formValues, locale);
@@ -428,7 +427,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
             provider.createEntry(guestUserDN, createObjectClasses, createAttributes);
             LOGGER.info(pwmSession, "created user object: " + guestUserDN);
 
-            final ChaiUser theUser = ChaiFactory.createChaiUser(guestUserDN, provider);
+            final ChaiUser theUser = provider.getEntryFactory().newChaiUser(guestUserDN);
             final UserIdentity userIdentity = new UserIdentity(guestUserDN, pwmSession.getUserInfo().getUserIdentity().getLdapProfileID());
 
             // write the expiration date:
@@ -440,17 +439,6 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
             final PwmPasswordPolicy passwordPolicy = PasswordUtility.readPasswordPolicyForUser(pwmApplication, pwmSession.getLabel(), userIdentity, theUser, locale);
             final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword(pwmSession.getLabel(), passwordPolicy, pwmApplication);
             theUser.setPassword(newPassword.getStringValue());
-            /*
-            final UserInfoBean guestUserInfoBean = new UserInfoBean();
-            final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication);
-            userStatusReader.populateUserInfoBean(
-                    pwmSession.getLabel(),
-                    guestUserInfoBean,
-                    pwmSession.getSessionStateBean().getLocale(),
-                    userIdentity,
-                    theUser.getChaiProvider()
-            );
-            */
 
 
             {  // execute configured actions
@@ -487,7 +475,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
         }
     }
 
-    private static Date readExpirationFromRequest(
+    private static Instant readExpirationFromRequest(
             final PwmRequest pwmRequest
     )
             throws PwmOperationalException, ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException
@@ -518,15 +506,15 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
 
         final long durationValueMs = durationValueDays * 24 * 60 * 60 * 1000;
         final long futureDateMs = System.currentTimeMillis() + durationValueMs;
-        final Date futureDate = new Date(futureDateMs);
+        final Instant futureDate = Instant.ofEpochMilli(futureDateMs);
 
-        if (expirationDate.after(futureDate)) {
+        if (expirationDate.after(Date.from(futureDate))) {
             final String errorMsg = "expiration date must be sooner than " + futureDate.toString();
             throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_FIELD_REQUIRED,errorMsg));
         }
 
         LOGGER.trace(pwmRequest, "read expiration date as " + expirationDate.toString());
-        return expirationDate;
+        return expirationDate.toInstant();
     }
 
     private static String determineUserDN(final Map<FormConfiguration, String> formValues, final Configuration config)

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

@@ -643,7 +643,7 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
 
         if (updateAttributesProfile.readSettingAsBoolean(PwmSetting.UPDATE_PROFILE_EMAIL_VERIFICATION)) {
             final String emailAddressAttribute = ldapProfile.readSettingAsString(PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE);
-            if (userFormData.containsKey(emailAddressAttribute)) {
+            if (userFormData.containsKey(emailAddressAttribute) && !userFormData.get(emailAddressAttribute).isEmpty()) {
                 ldapData = formDataFromLdap(pwmRequest, updateAttributesProfile);
                 if (userFormData.get(emailAddressAttribute) != null && !userFormData.get(emailAddressAttribute).equalsIgnoreCase(ldapData.get(emailAddressAttribute))) {
                     returnObj.add(TokenVerificationProgress.TokenChannel.EMAIL);
@@ -655,7 +655,7 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
 
         if (updateAttributesProfile.readSettingAsBoolean(PwmSetting.UPDATE_PROFILE_SMS_VERIFICATION)) {
             final String phoneNumberAttribute = ldapProfile.readSettingAsString(PwmSetting.SMS_USER_PHONE_ATTRIBUTE);
-            if (userFormData.containsKey(phoneNumberAttribute)) {
+            if (userFormData.containsKey(phoneNumberAttribute) && !userFormData.get(phoneNumberAttribute).isEmpty()) {
                 if (ldapData == null) {
                     ldapData = formDataFromLdap(pwmRequest, updateAttributesProfile);
                 }

+ 24 - 238
server/src/main/java/password/pwm/http/servlet/accountinfo/AccountInformationBean.java

@@ -3,6 +3,7 @@ package password.pwm.http.servlet.accountinfo;
 import lombok.Builder;
 import lombok.Value;
 import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.ViewStatusFields;
 import password.pwm.config.profile.PwmPasswordPolicy;
@@ -11,12 +12,10 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.bean.DisplayElement;
 import password.pwm.http.tag.PasswordRequirementsTag;
-import password.pwm.i18n.Display;
 import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.ViewableUserInfoDisplayReader;
 import password.pwm.svc.event.UserAuditRecord;
-import password.pwm.util.LocaleHelper;
 import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -29,7 +28,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 
 @Value
 @Builder
@@ -58,9 +56,20 @@ public class AccountInformationBean implements Serializable {
         final Instant startTime = Instant.now();
         final AccountInformationBeanBuilder builder = new AccountInformationBean.AccountInformationBeanBuilder();
 
-        builder.accountInfo(makeAccountInfo(pwmRequest, userInfo, locale));
+        builder.accountInfo(ViewableUserInfoDisplayReader.makeDisplayData(
+                pwmRequest.getConfig().readSettingAsOptionList(PwmSetting.ACCOUNT_INFORMATION_VIEW_STATUS_VALUES,ViewStatusFields.class),
+                pwmRequest.getConfig(),
+                userInfo,
+                pwmRequest.getPwmSession().getSessionStateBean(),
+                locale
+        ));
         builder.formData(makeFormInfo(pwmRequest, locale));
-        builder.auditData(makeAuditInfo(pwmRequest));
+        builder.auditData(makeAuditInfo(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                userInfo,
+                pwmRequest.getLocale()
+                ));
         builder.passwordRules(makePasswordRules(pwmRequest));
 
         LOGGER.trace(pwmRequest, "generated account information bean in " + TimeDuration.compactFromCurrent(startTime));
@@ -77,26 +86,29 @@ public class AccountInformationBean implements Serializable {
         return Collections.unmodifiableList(rules);
     }
 
-    private static List<ActivityRecord> makeAuditInfo(
-            final PwmRequest pwmRequest
+    public static List<ActivityRecord> makeAuditInfo(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final Locale locale
     ) {
 
-        if (!pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.ACCOUNT_INFORMATION_HISTORY)) {
+        if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.ACCOUNT_INFORMATION_HISTORY)) {
             return Collections.emptyList();
         }
 
         final List<UserAuditRecord> auditRecords = new ArrayList<>();
         try {
-            auditRecords.addAll(pwmRequest.getPwmApplication().getAuditManager().readUserHistory(pwmRequest.getPwmSession()));
+            auditRecords.addAll(pwmApplication.getAuditManager().readUserHistory(userInfo));
         } catch (PwmUnrecoverableException e) {
-            LOGGER.debug(pwmRequest, "error reading audit data for user: " + e.getMessage());
+            LOGGER.debug(sessionLabel, "error reading audit data for user: " + e.getMessage());
         }
 
         final List<ActivityRecord> returnData  = new ArrayList<>();
         for (final UserAuditRecord userAuditRecord : auditRecords) {
             returnData.add(new ActivityRecord(
                     userAuditRecord.getTimestamp(),
-                    userAuditRecord.getEventCode().getLocalizedString(pwmRequest.getConfig(), pwmRequest.getLocale())
+                    userAuditRecord.getEventCode().getLocalizedString(pwmApplication.getConfig(), locale)
             ));
         }
 
@@ -138,230 +150,4 @@ public class AccountInformationBean implements Serializable {
 
         return Collections.unmodifiableList(returnData);
     }
-
-
-    private static List<DisplayElement> makeAccountInfo(
-            final PwmRequest pwmRequest,
-            final UserInfo userInfo,
-            final Locale locale
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final List<DisplayElement> accountInfo = new ArrayList<>();
-        final DataElementMaker maker = new DataElementMaker(pwmApplication, locale, accountInfo);
-
-        maker.add(
-                ViewStatusFields.Username,
-                Display.Field_Username,
-                userInfo.getUsername()
-        );
-
-        maker.add(
-                ViewStatusFields.UserDN,
-                Display.Field_UserDN,
-                userInfo.getUserIdentity().getUserDN()
-        );
-
-        if (pwmApplication.getConfig().getLdapProfiles().size() > 1) {
-            final String ldapProfileID = userInfo.getUserIdentity().getLdapProfileID();
-            final String value = pwmApplication.getConfig().getLdapProfiles().get(ldapProfileID).getDisplayName(locale);
-            maker.add(
-                    ViewStatusFields.UserDN,
-                    Display.Field_LdapProfile,
-                    value
-            );
-        }
-
-        maker.add(
-                ViewStatusFields.UserEmail,
-                Display.Field_UserEmail,
-                userInfo.getUserEmailAddress()
-        );
-
-        maker.add(
-                ViewStatusFields.UserSMS,
-                Display.Field_UserSMS,
-                userInfo.getUserSmsNumber()
-        );
-
-        maker.add(
-                ViewStatusFields.GUID,
-                Display.Field_UserGUID,
-                userInfo.getUserGuid()
-        );
-
-        maker.add(
-                ViewStatusFields.AccountExpirationTime,
-                Display.Field_AccountExpirationTime,
-                userInfo.getAccountExpirationTime()
-        );
-
-        maker.add(
-                ViewStatusFields.PasswordExpired,
-                Display.Field_PasswordExpired,
-                userInfo.getPasswordStatus().isExpired()
-        );
-
-        maker.add(
-                ViewStatusFields.PasswordPreExpired,
-                Display.Field_PasswordPreExpired,
-                userInfo.getPasswordStatus().isPreExpired()
-        );
-
-        maker.add(
-                ViewStatusFields.PasswordWarnPeriod,
-                Display.Field_PasswordWithinWarningPeriod,
-                userInfo.getPasswordStatus().isWarnPeriod()
-        );
-
-        maker.add(
-                ViewStatusFields.PasswordViolatesPolicy,
-                Display.Field_PasswordViolatesPolicy,
-                userInfo.getPasswordStatus().isViolatesPolicy()
-        );
-
-        maker.add(
-                ViewStatusFields.PasswordSetTime,
-                Display.Field_PasswordSetTime,
-                userInfo.getPasswordLastModifiedTime()
-        );
-
-        {
-            final String value = userInfo.getPasswordLastModifiedTime() != null
-                    ? TimeDuration.fromCurrent(userInfo.getPasswordLastModifiedTime()).asLongString(locale)
-                    : LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, pwmApplication.getConfig());
-            maker.add(
-                    ViewStatusFields.PasswordSetTimeDelta,
-                    Display.Field_PasswordSetTimeDelta,
-                    value
-            );
-        }
-
-        maker.add(
-                ViewStatusFields.PasswordExpireTime,
-                Display.Field_PasswordExpirationTime,
-                userInfo.getPasswordExpirationTime()
-        );
-
-        maker.add(
-                ViewStatusFields.ResponsesStored,
-                Display.Field_ResponsesStored,
-                userInfo.getResponseInfoBean() != null
-        );
-
-        maker.add(
-                ViewStatusFields.ResponsesStored,
-                Display.Field_ResponsesStored,
-                userInfo.getResponseInfoBean() != null
-        );
-
-        if (userInfo.getResponseInfoBean() != null) {
-            maker.add(
-                    ViewStatusFields.ResponsesTimestamp,
-                    Display.Field_ResponsesTimestamp,
-                    userInfo.getResponseInfoBean().getTimestamp()
-            );
-        }
-
-        if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.OTP_ENABLED)) {
-            maker.add(
-                    ViewStatusFields.OTPStored,
-                    Display.Field_OTP_Stored,
-                    userInfo.getOtpUserRecord() != null
-            );
-
-            if (userInfo.getOtpUserRecord() != null) {
-                maker.add(
-                        ViewStatusFields.OTPTimestamp,
-                        Display.Field_OTP_Timestamp,
-                        userInfo.getOtpUserRecord().getTimestamp()
-                );
-            }
-        }
-
-        maker.add(
-                ViewStatusFields.NetworkAddress,
-                Display.Field_NetworkAddress,
-                pwmRequest.getPwmSession().getSessionStateBean().getSrcAddress()
-        );
-
-        maker.add(
-                ViewStatusFields.NetworkHost,
-                Display.Field_NetworkHost,
-                pwmRequest.getPwmSession().getSessionStateBean().getSrcHostname()
-        );
-
-        maker.add(
-                ViewStatusFields.LogoutURL,
-                Display.Field_LogoutURL,
-                pwmRequest.getLogoutURL()
-        );
-
-        maker.add(
-                ViewStatusFields.ForwardURL,
-                Display.Field_ForwardURL,
-                pwmRequest.getForwardUrl()
-        );
-
-        return Collections.unmodifiableList(accountInfo);
-    }
-
-    private static class DataElementMaker {
-        private final PwmApplication pwmApplication;
-        private final Locale locale;
-        private final List<DisplayElement> list;
-
-        DataElementMaker(final PwmApplication pwmApplication, final Locale locale, final List<DisplayElement> list) {
-            this.pwmApplication = pwmApplication;
-            this.locale = locale;
-            this.list = list;
-        }
-
-        void add(final ViewStatusFields viewStatusField, final Display display, final Instant instant) {
-            final Set<ViewStatusFields> viewStatusFields = pwmApplication.getConfig().readSettingAsOptionList(PwmSetting.ACCOUNT_INFORMATION_VIEW_STATUS_VALUES,ViewStatusFields.class);
-
-            if (!viewStatusFields.contains(viewStatusField)) {
-                return;
-            }
-
-            final String strValue = instant == null
-                    ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, pwmApplication.getConfig())
-                    : JavaHelper.toIsoDate(instant);
-
-            list.add(new DisplayElement(
-                    display.name(),
-                    DisplayElement.Type.timestamp,
-                    LocaleHelper.getLocalizedMessage(locale, display, pwmApplication.getConfig()),
-                    strValue
-            ));
-        }
-
-        void add(final ViewStatusFields viewStatusField, final Display display, final boolean value) {
-            add(viewStatusField, display, LocaleHelper.booleanString(
-                    value,
-                    locale,
-                    pwmApplication.getConfig()
-            ));
-        }
-
-        void add(final ViewStatusFields viewStatusField, final Display display, final String value) {
-            final Set<ViewStatusFields> viewStatusFields = pwmApplication.getConfig().readSettingAsOptionList(PwmSetting.ACCOUNT_INFORMATION_VIEW_STATUS_VALUES,ViewStatusFields.class);
-
-            if (!viewStatusFields.contains(viewStatusField)) {
-                return;
-            }
-
-            final String strValue = StringUtil.isEmpty(value)
-                    ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, pwmApplication.getConfig())
-                    : value;
-
-            list.add(new DisplayElement(
-                    display.name(),
-                    DisplayElement.Type.string,
-                    LocaleHelper.getLocalizedMessage(locale, display, pwmApplication.getConfig()),
-                    strValue
-            ));
-        }
-    }
 }

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

@@ -151,8 +151,7 @@ public class AppDashboardData implements Serializable {
     }
 
     private static int ldapConnectionCount(final PwmApplication pwmApplication) {
-        return pwmApplication.getSessionTrackService().ldapConnectionCount()
-                + pwmApplication.getLdapConnectionService().connectionCount();
+        return pwmApplication.getLdapConnectionService().connectionCount();
     }
 
     private static List<DisplayElement> makeAboutData(

+ 4 - 1
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -876,7 +876,10 @@ public class ConfigEditorServlet extends ControlledPwmServlet {
         final String profile = inputMap.get("profile");
         final String dn = inputMap.containsKey("dn") ? inputMap.get("dn") : "";
 
-        final LdapBrowser ldapBrowser = new LdapBrowser(configManagerBean.getStoredConfiguration());
+        final LdapBrowser ldapBrowser = new LdapBrowser(
+                pwmRequest.getPwmApplication().getLdapConnectionService().getChaiProviderFactory(),
+                configManagerBean.getStoredConfiguration()
+        );
 
         LdapBrowser.LdapBrowseResult result;
         try {

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

@@ -330,7 +330,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet {
     private ProcessStatus restBrowseLdap(
             final PwmRequest pwmRequest
     )
-            throws IOException, ServletException, PwmUnrecoverableException
+            throws IOException, PwmUnrecoverableException
     {
         final ConfigGuideBean configGuideBean = getBean(pwmRequest);
 
@@ -345,7 +345,10 @@ public class ConfigGuideServlet extends ControlledPwmServlet {
         final String profile = inputMap.get("profile");
         final String dn = inputMap.getOrDefault("dn", "");
 
-        final LdapBrowser ldapBrowser = new LdapBrowser(storedConfiguration);
+        final LdapBrowser ldapBrowser = new LdapBrowser(
+                pwmRequest.getPwmApplication().getLdapConnectionService().getChaiProviderFactory(),
+                storedConfiguration
+        );
         final LdapBrowser.LdapBrowseResult result = ldapBrowser.doBrowse(profile, dn);
         ldapBrowser.close();
 
@@ -445,7 +448,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet {
         final ConfigGuideBean configGuideBean = getBean(pwmRequest);
 
         try {
-            final SchemaOperationResult schemaOperationResult = ConfigGuideUtils.extendSchema(configGuideBean, true);
+            final SchemaOperationResult schemaOperationResult = ConfigGuideUtils.extendSchema(pwmRequest.getPwmApplication(), configGuideBean, true);
             pwmRequest.outputJsonResult(RestResultBean.withData(schemaOperationResult.getOperationLog()));
         } catch (Exception e) {
             final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN,e.getMessage());

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

@@ -2,7 +2,6 @@ package password.pwm.http.servlet.configguide;
 
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
-import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import password.pwm.PwmApplication;
@@ -84,14 +83,24 @@ public class ConfigGuideUtils {
         }
     }
 
-    public static SchemaOperationResult extendSchema(final ConfigGuideBean configGuideBean, final boolean doSchemaExtension) {
+    public static SchemaOperationResult extendSchema(
+            final PwmApplication pwmApplication,
+            final ConfigGuideBean configGuideBean,
+            final boolean doSchemaExtension
+    ) {
         final Map<ConfigGuideFormField,String> form = configGuideBean.getFormData();
         final boolean ldapServerSecure = "true".equalsIgnoreCase(form.get(ConfigGuideFormField.PARAM_LDAP_SECURE));
         final String ldapUrl = "ldap" + (ldapServerSecure ? "s" : "") + "://" + form.get(ConfigGuideFormField.PARAM_LDAP_HOST) + ":" + form.get(ConfigGuideFormField.PARAM_LDAP_PORT);
         try {
-            final ChaiConfiguration chaiConfiguration = new ChaiConfiguration(ldapUrl, form.get(ConfigGuideFormField.PARAM_LDAP_PROXY_DN), form.get(ConfigGuideFormField.PARAM_LDAP_PROXY_PW));
-            chaiConfiguration.setSetting(ChaiSetting.PROMISCUOUS_SSL,"true");
-            final ChaiProvider chaiProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
+            final ChaiConfiguration chaiConfiguration = ChaiConfiguration.builder(
+                    ldapUrl,
+                    form.get(ConfigGuideFormField.PARAM_LDAP_PROXY_DN),
+                    form.get(ConfigGuideFormField.PARAM_LDAP_PROXY_PW)
+            )
+                    .setSetting(ChaiSetting.PROMISCUOUS_SSL,"true")
+                    .build();
+
+            final ChaiProvider chaiProvider = pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(chaiConfiguration);
             if (doSchemaExtension) {
                 return SchemaManager.extendSchema(chaiProvider);
             } else {

+ 1 - 0
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -361,6 +361,7 @@ public class DebugItemGenerator {
         @Override
         public void outputItem(final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream) throws Exception {
             final List<LdapDebugDataGenerator.LdapDebugInfo> ldapDebugInfos = LdapDebugDataGenerator.makeLdapDebugInfos(
+                    pwmApplication,
                     pwmRequest.getSessionLabel(),
                     pwmApplication.getConfig(),
                     pwmRequest.getLocale()

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

@@ -204,7 +204,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
 
         // convert a url command like /public/newuser/12321321 to redirect with a process action.
         if (action == null) {
-            if (pwmRequest.convertURLtokenCommand()) {
+            if (pwmRequest.convertURLtokenCommand(PwmServletDefinition.ForgottenPassword, ForgottenPasswordAction.enterCode)) {
                 return ProcessStatus.Halt;
             }
         }
@@ -860,9 +860,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
                 final Set<IdentityVerificationMethod> remainingAvailableOptionalMethods = ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods(pwmRequest, forgottenPasswordBean);
                 if (remainingAvailableOptionalMethods.isEmpty()) {
                     final String errorMsg = "additional optional verification methods are needed, however all available optional verification methods have been satisified by user";
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
+                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT,errorMsg);
                     LOGGER.error(pwmRequest, errorInformation);
-                    throw new PwmUnrecoverableException(errorInformation);
+                    pwmRequest.respondWithError(errorInformation);
+                    return;
                 } else {
                     if (remainingAvailableOptionalMethods.size() == 1) {
                         final IdentityVerificationMethod remainingMethod = remainingAvailableOptionalMethods.iterator().next();

+ 43 - 6
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -58,6 +58,7 @@ import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
@@ -279,12 +280,8 @@ class ForgottenPasswordUtil {
     {
         switch (recoveryVerificationMethods) {
             case TOKEN: {
-                final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod();
-                if (tokenSendMethod == null || tokenSendMethod == MessageSendMethod.NONE) {
-                    final String errorMsg = "user is required to complete token validation, yet there is not a token send method configured";
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG, errorMsg);
-                    throw new PwmUnrecoverableException(errorInformation);
-                }
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo(pwmRequest, forgottenPasswordBean);
+                figureIfTokenSendMethodIsAvailableForUser(forgottenPasswordBean, userInfo);
             }
             break;
 
@@ -340,6 +337,46 @@ class ForgottenPasswordUtil {
         }
     }
 
+    private static void figureIfTokenSendMethodIsAvailableForUser(
+            final ForgottenPasswordBean forgottenPasswordBean,
+            final UserInfo userInfoBean
+    )
+            throws PwmUnrecoverableException
+    {
+        final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod();
+        if (tokenSendMethod == null || tokenSendMethod == MessageSendMethod.NONE) {
+            final String errorMsg = "user is required to complete token validation, yet there is not a token send method configured";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG, errorMsg);
+            throw new PwmUnrecoverableException(errorInformation);
+        }
+
+        final boolean hasEmailAddr = !StringUtil.isEmpty(userInfoBean.getUserEmailAddress());
+        final boolean hasSmsAddr = !StringUtil.isEmpty(userInfoBean.getUserSmsNumber());
+
+        if (tokenSendMethod == MessageSendMethod.EMAILONLY && !hasEmailAddr) {
+            final String errorMsg = "token send method requires an email address, yet user does not have an email address value";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg);
+            throw new PwmUnrecoverableException(errorInformation);
+        }
+
+        if (tokenSendMethod == MessageSendMethod.SMSONLY && !hasSmsAddr) {
+            final String errorMsg = "token send method requires an sms destination, yet user does not have an sms destination value";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg);
+            throw new PwmUnrecoverableException(errorInformation);
+        }
+
+        if (tokenSendMethod == MessageSendMethod.CHOICE_SMS_EMAIL
+                || tokenSendMethod == MessageSendMethod.EMAILFIRST
+                || tokenSendMethod == MessageSendMethod.SMSFIRST)
+        {
+            if (!hasEmailAddr && !hasSmsAddr) {
+                final String errorMsg = "token send method requires an sms or email desitnation, yet user does not have an sms or email destination value";
+                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg);
+                throw new PwmUnrecoverableException(errorInformation);
+            }
+        }
+    }
+
     static Map<Challenge, String> readResponsesFromHttpRequest(
             final PwmRequest req,
             final ChallengeSet challengeSet

+ 199 - 65
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java

@@ -24,24 +24,32 @@ package password.pwm.http.servlet.helpdesk;
 
 import com.novell.ldapchai.ChaiPasswordRule;
 import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.cr.Challenge;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
 import lombok.Getter;
 import lombok.Setter;
+
 import password.pwm.bean.ResponseInfoBean;
 import password.pwm.bean.UserIdentity;
-import password.pwm.bean.pub.PublicUserInfoBean;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.option.HelpdeskUIMode;
+import password.pwm.config.option.ViewStatusFields;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.DisplayElement;
+import password.pwm.http.servlet.accountinfo.AccountInformationBean;
 import password.pwm.http.tag.PasswordRequirementsTag;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
-import password.pwm.svc.event.UserAuditRecord;
+import password.pwm.ldap.ViewableUserInfoDisplayReader;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
@@ -50,59 +58,71 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 
-import javax.servlet.ServletException;
-import java.io.IOException;
 import java.io.Serializable;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 @Getter
 @Setter(AccessLevel.PRIVATE)
 public class HelpdeskDetailInfoBean implements Serializable {
     private static final PwmLogger LOGGER = PwmLogger.forClass(HelpdeskDetailInfoBean.class);
 
-    private PublicUserInfoBean userInfo;
     private String userDisplayName;
 
-    private boolean intruderLocked;
-    private boolean accountEnabled;
-    private boolean accountExpired;
-
-    private Instant lastLoginTime;
-    private List<UserAuditRecord> userHistory;
-    private Map<FormConfiguration, List<String>> searchDetails;
-    private String passwordSetDelta;
+    private List<AccountInformationBean.ActivityRecord> userHistory;
 
     private Map<String, String> passwordPolicyRules;
     private List<String> passwordRequirements;
     private String passwordPolicyDN;
     private String passwordPolicyID;
 
-    private boolean hasOtpRecord;
-    private String otpRecordTimestamp;
+    private List<DisplayElement> statusData;
+    private List<DisplayElement> profileData;
+    private List<DisplayElement> helpdeskResponses;
+
+    private Set<StandardButton> visibleButtons;
+    private Set<StandardButton> enabledButtons;
+    private List<ButtonInfo> customButtons;
 
-    private ResponseInfoBean responseInfoBean;
+    @Data
+    @AllArgsConstructor
+    public static class ButtonInfo implements Serializable {
+        private String name;
+        private String label;
+        private String description;
+    }
 
-    private transient UserInfo backingUserInfo;
+    public enum StandardButton {
+        back,
+        refresh,
+        changePassword,
+        unlock,
+        clearResponses,
+        clearOtpSecret,
+        verification,
+        deleteUser,
+    }
 
     static HelpdeskDetailInfoBean makeHelpdeskDetailInfo(
             final PwmRequest pwmRequest,
             final HelpdeskProfile helpdeskProfile,
             final UserIdentity userIdentity
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
+            throws PwmUnrecoverableException, ChaiUnavailableException
     {
         final Instant startTime = Instant.now();
         LOGGER.trace(pwmRequest, "beginning to assemble detail data report for user " + userIdentity);
         final Locale actorLocale = pwmRequest.getLocale();
         final ChaiUser theUser = HelpdeskServlet.getChaiUser(pwmRequest, helpdeskProfile, userIdentity);
 
-        if (!theUser.isValid()) {
+        if (!theUser.exists()) {
             return null;
         }
 
@@ -116,50 +136,43 @@ public class HelpdeskDetailInfoBean implements Serializable {
         );
         final MacroMachine macroMachine = new MacroMachine(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null);
 
-        detailInfo.setUserInfo(PublicUserInfoBean.fromUserInfoBean(userInfo, pwmRequest.getConfig(), pwmRequest.getLocale(), macroMachine));
-
-        try {
-            detailInfo.setIntruderLocked(theUser.isPasswordLocked());
-        } catch (Exception e) {
-            LOGGER.error(pwmRequest, "unexpected error reading intruder lock status for user '" + userIdentity + "', " + e.getMessage());
-        }
-
-        try {
-            detailInfo.setAccountEnabled(theUser.isAccountEnabled());
-        } catch (Exception e) {
-            LOGGER.error(pwmRequest, "unexpected error reading account enabled status for user '" + userIdentity + "', " + e.getMessage());
-        }
-
-        try {
-            detailInfo.setAccountExpired(theUser.isAccountExpired());
-        } catch (Exception e) {
-            LOGGER.error(pwmRequest, "unexpected error reading account expired status for user '" + userIdentity + "', " + e.getMessage());
-        }
-
-        try {
-            final Date lastLoginTime = theUser.readLastLoginTime();
-            detailInfo.setLastLoginTime(lastLoginTime == null ? null : lastLoginTime.toInstant());
-        } catch (Exception e) {
-            LOGGER.error(pwmRequest, "unexpected error reading last login time for user '" + userIdentity + "', " + e.getMessage());
-        }
-
         try {
-            detailInfo.setUserHistory(pwmRequest.getPwmApplication().getAuditManager().readUserHistory(userInfo));
+            detailInfo.userHistory = AccountInformationBean.makeAuditInfo(
+                    pwmRequest.getPwmApplication(),
+                    pwmRequest.getSessionLabel(),
+                    userInfo,
+                    pwmRequest.getLocale()
+            );
         } catch (Exception e) {
             LOGGER.error(pwmRequest, "unexpected error reading userHistory for user '" + userIdentity + "', " + e.getMessage());
         }
 
-        if (detailInfo.getUserInfo().getPasswordLastModifiedTime() != null) {
-            final TimeDuration passwordSetDelta = TimeDuration.fromCurrent(detailInfo.getUserInfo().getPasswordLastModifiedTime());
-            detailInfo.setPasswordSetDelta(passwordSetDelta.asLongString(pwmRequest.getLocale()));
-        } else {
-            detailInfo.setPasswordSetDelta(LocaleHelper.getLocalizedMessage(Display.Value_NotApplicable, pwmRequest));
-        }
-
         {
             final List<FormConfiguration> detailFormConfig = helpdeskProfile.readSettingAsForm(PwmSetting.HELPDESK_DETAIL_FORM);
             final Map<FormConfiguration, List<String>> formData = FormUtility.populateFormMapFromLdap(detailFormConfig, pwmRequest.getPwmSession().getLabel(), userInfo);
-            detailInfo.setSearchDetails(formData);
+            final List<DisplayElement> profileData = new ArrayList<>();
+            for (final Map.Entry<FormConfiguration, List<String>> entry : formData.entrySet()) {
+                final FormConfiguration formConfiguration = entry.getKey();
+                if (formConfiguration.isMultivalue()) {
+                    profileData.add(new DisplayElement(
+                            formConfiguration.getName(),
+                            DisplayElement.Type.multiString,
+                            formConfiguration.getLabel(actorLocale),
+                            entry.getValue()
+                    ));
+                } else {
+                    final String value = JavaHelper.isEmpty(entry.getValue())
+                            ? ""
+                            : entry.getValue().iterator().next();
+                    profileData.add(new DisplayElement(
+                            formConfiguration.getName(),
+                            DisplayElement.Type.string,
+                            formConfiguration.getLabel(actorLocale),
+                            value
+                    ));
+                }
+            }
+            detailInfo.profileData = profileData;
         }
 
         {
@@ -207,17 +220,26 @@ public class HelpdeskDetailInfoBean implements Serializable {
             detailInfo.setPasswordPolicyID(LocaleHelper.getLocalizedMessage(Display.Value_NotApplicable, pwmRequest));
         }
 
-        detailInfo.hasOtpRecord = userInfo.getOtpUserRecord() != null;
-
-        detailInfo.otpRecordTimestamp = userInfo.getOtpUserRecord() != null && userInfo.getOtpUserRecord().getTimestamp() != null
-                ? JavaHelper.toIsoDate(userInfo.getOtpUserRecord().getTimestamp())
-                : LocaleHelper.getLocalizedMessage(Display.Value_NotApplicable, pwmRequest);
-
-        detailInfo.responseInfoBean = userInfo.getResponseInfoBean();
+        {
+            final ResponseInfoBean responseInfoBean = userInfo.getResponseInfoBean();
+            if (responseInfoBean != null && responseInfoBean.getHelpdeskCrMap() != null) {
+                final List<DisplayElement> responseDisplay = new ArrayList<>();
+                int counter = 0;
+                for (final Map.Entry<Challenge, String> entry : responseInfoBean.getHelpdeskCrMap().entrySet()) {
+                    counter++;
+                    responseDisplay.add(new DisplayElement(
+                            "item_" + counter,
+                            DisplayElement.Type.string,
+                            entry.getKey().getChallengeText(),
+                            entry.getValue()
+                    ));
+                }
+                detailInfo.helpdeskResponses = responseDisplay;
+            }
 
-        detailInfo.setBackingUserInfo(userInfo);
+        }
 
-            final String configuredDisplayName = helpdeskProfile.readSettingAsString(PwmSetting.HELPDESK_DETAIL_DISPLAY_NAME);
+        final String configuredDisplayName = helpdeskProfile.readSettingAsString(PwmSetting.HELPDESK_DETAIL_DISPLAY_NAME);
         if (configuredDisplayName != null && !configuredDisplayName.isEmpty()) {
             final String displayName = macroMachine.expandMacros(configuredDisplayName);
             detailInfo.setUserDisplayName(displayName);
@@ -229,8 +251,120 @@ public class HelpdeskDetailInfoBean implements Serializable {
                     + " in " + timeDuration.asCompactString() + ", contents: " + JsonUtil.serialize(detailInfo));
         }
 
+        {
+            final Set<ViewStatusFields> viewStatusFields = helpdeskProfile.readSettingAsOptionList(PwmSetting.HELPDESK_VIEW_STATUS_VALUES, ViewStatusFields.class);
+            detailInfo.statusData = ViewableUserInfoDisplayReader.makeDisplayData(
+                    viewStatusFields,
+                    pwmRequest.getConfig(),
+                    userInfo,
+                    null,
+                    pwmRequest.getLocale()
+            );
+        }
+
+        detailInfo.setVisibleButtons(determineVisibleButtons(pwmRequest, helpdeskProfile));
+        detailInfo.setEnabledButtons(determineEnabledButtons(detailInfo.getVisibleButtons(), userInfo));
+        detailInfo.setCustomButtons(determineCustomButtons(helpdeskProfile));
 
         return detailInfo;
     }
+
+    static Set<StandardButton> determineVisibleButtons(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile
+    )
+    {
+        final Set<StandardButton> buttons = new LinkedHashSet<>();
+
+        buttons.add(StandardButton.refresh);
+        buttons.add(StandardButton.back);
+
+        {
+            final HelpdeskUIMode uiMode =
+                    helpdeskProfile.readSettingAsEnum(PwmSetting.HELPDESK_SET_PASSWORD_MODE, HelpdeskUIMode.class);
+            if (uiMode != HelpdeskUIMode.none) {
+                buttons.add(StandardButton.changePassword);
+            }
+        }
+
+        if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_ENABLE_UNLOCK)) {
+            buttons.add(StandardButton.unlock);
+        }
+
+        if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_CLEAR_RESPONSES_BUTTON)) {
+            buttons.add(StandardButton.clearResponses);
+        }
+
+        if (pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.OTP_ENABLED)) {
+            if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_CLEAR_OTP_BUTTON)) {
+                buttons.add(StandardButton.clearOtpSecret);
+            }
+        }
+
+        if (!helpdeskProfile.readOptionalVerificationMethods().isEmpty()) {
+            buttons.add(StandardButton.verification);
+        }
+
+        if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_DELETE_USER_BUTTON)) {
+            buttons.add(StandardButton.deleteUser);
+        }
+
+        return Collections.unmodifiableSet(buttons);
+    }
+
+    static Set<StandardButton> determineEnabledButtons(
+            final Set<StandardButton> visibleButtons,
+            final UserInfo userInfo
+    )
+            throws PwmUnrecoverableException
+    {
+        final Set<StandardButton> buttons = new LinkedHashSet<>(visibleButtons);
+
+        if (buttons.contains(StandardButton.unlock)) {
+            final boolean enabled = userInfo.isPasswordLocked();
+            if (!enabled) {
+                buttons.remove(StandardButton.unlock);
+            }
+        }
+
+        if (buttons.contains(StandardButton.clearResponses)) {
+            final boolean enabled = userInfo.getResponseInfoBean() != null;
+            if (!enabled) {
+                buttons.remove(StandardButton.clearResponses);
+            }
+        }
+
+        if (buttons.contains(StandardButton.clearOtpSecret)) {
+            final boolean enabled = userInfo.getOtpUserRecord() != null;
+            if (!enabled) {
+                buttons.remove(StandardButton.clearOtpSecret);
+            }
+        }
+
+        return Collections.unmodifiableSet(buttons);
+    }
+
+    static List<ButtonInfo> determineCustomButtons(
+            final HelpdeskProfile helpdeskProfile
+    )
+    {
+        final List<ActionConfiguration> actions = helpdeskProfile.readSettingAsAction(PwmSetting.HELPDESK_ACTIONS);
+
+        final List<ButtonInfo> buttons = new ArrayList<>();
+        if (actions != null) {
+            int count = 0;
+            for (final ActionConfiguration action : actions) {
+                buttons.add(new ButtonInfo(
+                        "custom_" + count++,
+                        action.getName(),
+                        action.getDescription()
+                ));
+            }
+        }
+
+        return Collections.unmodifiableList(buttons);
+
+    }
+
 }
 

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

@@ -537,14 +537,13 @@ public class HelpdeskServlet extends ControlledPwmServlet {
             intruderManager.convenience().clearUserIdentity(userIdentity);
         }
 
-        // send notice email
-        HelpdeskServletUtil.sendUnlockNoticeEmail(pwmRequest, helpdeskProfile, userIdentity);
 
-        final boolean useProxy = helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_USE_PROXY);
         try {
-            final ChaiUser chaiUser = useProxy ?
-                    pwmRequest.getPwmApplication().getProxiedChaiUser(userIdentity) :
-                    pwmRequest.getPwmSession().getSessionManager().getActor(pwmRequest.getPwmApplication(), userIdentity);
+            final ChaiUser chaiUser = getChaiUser(pwmRequest, helpdeskProfile, userIdentity);
+
+            // send notice email
+            HelpdeskServletUtil.sendUnlockNoticeEmail(pwmRequest, helpdeskProfile, userIdentity, chaiUser);
+
             chaiUser.unlockPassword();
             {
                 // mark the event log
@@ -705,7 +704,13 @@ public class HelpdeskServlet extends ControlledPwmServlet {
             pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation, pwmRequest));
             return ProcessStatus.Halt;
         }
-        final UserInfo userInfo = helpdeskDetailInfoBean.getBackingUserInfo();
+        final UserInfo userInfo = UserInfoFactory.newUserInfo(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userIdentity,
+                getChaiUser(pwmRequest, helpdeskProfile, userIdentity).getChaiProvider()
+        );
         final MacroMachine macroMachine = new MacroMachine(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null);
         final String configuredTokenString = config.readAppProperty(AppProperty.HELPDESK_TOKEN_VALUE);
         final String tokenKey = macroMachine.expandMacros(configuredTokenString);

+ 20 - 9
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java

@@ -1,5 +1,6 @@
 package password.pwm.http.servlet.helpdesk;
 
+import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -18,6 +19,8 @@ import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.ldap.LdapPermissionTester;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
@@ -95,10 +98,9 @@ class HelpdeskServletUtil {
             return;
         }
 
-        if (helpdeskDetailInfoBean != null && helpdeskDetailInfoBean.getUserInfo() != null) {
-            final String obfuscatedDN = helpdeskDetailInfoBean.getBackingUserInfo().getUserIdentity().toObfuscatedKey(pwmRequest.getPwmApplication());
+        if (helpdeskDetailInfoBean != null) {
+            final String obfuscatedDN = userIdentity.toObfuscatedKey(pwmRequest.getPwmApplication());
             pwmRequest.setAttribute(PwmRequestAttribute.HelpdeskObfuscatedDN, obfuscatedDN);
-            pwmRequest.setAttribute(PwmRequestAttribute.HelpdeskUsername, helpdeskDetailInfoBean.getUserInfo().getUserID());
         }
 
         pwmRequest.setAttribute(PwmRequestAttribute.HelpdeskDetail, helpdeskDetailInfoBean);
@@ -111,7 +113,7 @@ class HelpdeskServletUtil {
             final HelpdeskProfile helpdeskProfile,
             final UserIdentity userIdentity
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
+            throws ChaiUnavailableException, PwmUnrecoverableException
     {
         final UserIdentity actorUserIdentity = pwmRequest.getUserInfoIfLoggedIn().canonicalized(pwmRequest.getPwmApplication());
 
@@ -175,9 +177,11 @@ class HelpdeskServletUtil {
     static void sendUnlockNoticeEmail(
             final PwmRequest pwmRequest,
             final HelpdeskProfile helpdeskProfile,
-            final UserIdentity userIdentity
+            final UserIdentity userIdentity,
+            final ChaiUser chaiUser
+
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException {
+            throws PwmUnrecoverableException, ChaiUnavailableException {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final Configuration config = pwmRequest.getConfig();
         final Locale locale = pwmRequest.getLocale();
@@ -188,17 +192,24 @@ class HelpdeskServletUtil {
             return;
         }
 
-        final HelpdeskDetailInfoBean helpdeskDetailInfoBean = HelpdeskDetailInfoBean.makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
+        final UserInfo userInfo = UserInfoFactory.newUserInfo(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userIdentity,
+                chaiUser.getChaiProvider()
+        );
+
         final MacroMachine macroMachine = new MacroMachine(
                 pwmApplication,
                 pwmRequest.getSessionLabel(),
-                helpdeskDetailInfoBean.getBackingUserInfo(),
+                userInfo,
                 null
         );
 
         pwmApplication.getEmailQueue().submitEmail(
                 configuredEmailSetting,
-                helpdeskDetailInfoBean.getBackingUserInfo(),
+                userInfo,
                 macroMachine
         );
     }

+ 11 - 4
server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java

@@ -164,7 +164,7 @@ public class NewUserServlet extends ControlledPwmServlet {
 
         // convert a url command like /public/newuser/12321321 to redirect with a process action.
         if (action == null) {
-            if (pwmRequest.convertURLtokenCommand()) {
+            if (pwmRequest.convertURLtokenCommand(PwmServletDefinition.NewUser, NewUserAction.enterCode)) {
                 return ProcessStatus.Halt;
             }
         } else if (action != NewUserAction.complete && action != NewUserAction.checkProgress ) {
@@ -237,7 +237,11 @@ public class NewUserServlet extends ControlledPwmServlet {
                 NewUserUtils.initializeToken(pwmRequest, newUserBean, TokenVerificationProgress.TokenChannel.EMAIL);
             }
 
-            if (!tokenVerificationProgress.getPassedTokens().contains(TokenVerificationProgress.TokenChannel.EMAIL)) {
+            if (!tokenVerificationProgress.getPassedTokens().contains(TokenVerificationProgress.TokenChannel.EMAIL)
+                    //if the token has been sent during the InitializeToken call, the issuedTokens member must contains the SMS key. If not, the token has not been sent (SMS number is null) and the verification phase should be ignored 
+                && tokenVerificationProgress.getIssuedTokens().contains(TokenVerificationProgress.TokenChannel.EMAIL)
+                ) 
+            {        
                 pwmRequest.forwardToJsp(JspUrl.NEW_USER_ENTER_CODE);
                 return;
             }
@@ -247,8 +251,11 @@ public class NewUserServlet extends ControlledPwmServlet {
             if (!newUserBean.getTokenVerificationProgress().getIssuedTokens().contains(TokenVerificationProgress.TokenChannel.SMS)) {
                 NewUserUtils.initializeToken(pwmRequest, newUserBean, TokenVerificationProgress.TokenChannel.SMS);
             }
-
-            if (!newUserBean.getTokenVerificationProgress().getPassedTokens().contains(TokenVerificationProgress.TokenChannel.SMS)) {
+            if (!newUserBean.getTokenVerificationProgress().getPassedTokens().contains(TokenVerificationProgress.TokenChannel.SMS)
+                //if the token has been sent during the InitializeToken call, the issuedTokens member must contains the SMS key. If not, the token has not been sent (SMS number is null) and the verification phase should be ignored 
+                && newUserBean.getTokenVerificationProgress().getIssuedTokens().contains(TokenVerificationProgress.TokenChannel.SMS)
+                ) 
+            {
                 pwmRequest.forwardToJsp(JspUrl.NEW_USER_ENTER_CODE);
                 return;
             }

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

@@ -22,14 +22,13 @@
 
 package password.pwm.http.servlet.newuser;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
-import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.bean.EmailItemBean;
@@ -37,13 +36,13 @@ import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
@@ -168,13 +167,13 @@ class NewUserUtils {
             throw new PwmOperationalException(errorInformation);
         }
 
-        final ChaiUser theUser = ChaiFactory.createChaiUser(newUserDN, chaiProvider);
+        final ChaiUser theUser = chaiProvider.getEntryFactory().newChaiUser(newUserDN);
 
         final boolean useTempPw;
         {
             final String settingValue = pwmApplication.getConfig().readAppProperty(AppProperty.NEWUSER_LDAP_USE_TEMP_PW);
             if ("auto".equalsIgnoreCase(settingValue)) {
-                useTempPw = chaiProvider.getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY;
+                useTempPw = chaiProvider.getDirectoryVendor() == DirectoryVendor.ACTIVE_DIRECTORY;
             } else {
                 useTempPw = Boolean.parseBoolean(settingValue);
             }
@@ -189,7 +188,7 @@ class NewUserUtils {
                         .build();
                 temporaryPassword = RandomPasswordGenerator.createRandomPassword(pwmSession.getLabel(), randomGeneratorConfig, pwmApplication);
             }
-            final ChaiUser proxiedUser = ChaiFactory.createChaiUser(newUserDN, chaiProvider);
+            final ChaiUser proxiedUser = chaiProvider.getEntryFactory().newChaiUser(newUserDN);
             try { //set password as admin
                 proxiedUser.setPassword(temporaryPassword.getStringValue());
                 NewUserUtils.LOGGER.debug(pwmSession, "set temporary password for new user entry: " + newUserDN);
@@ -201,7 +200,7 @@ class NewUserUtils {
             }
 
             // add AD-specific attributes
-            if (ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY == chaiProvider.getDirectoryVendor()) {
+            if (DirectoryVendor.ACTIVE_DIRECTORY == chaiProvider.getDirectoryVendor()) {
                 try {
                     NewUserUtils.LOGGER.debug(pwmSession,
                             "setting userAccountControl attribute to enable account " + theUser.getEntryDN());
@@ -217,11 +216,12 @@ class NewUserUtils {
             try { // bind as user
                 NewUserUtils.LOGGER.debug(pwmSession,
                         "attempting bind as user to then allow changing to requested password for new user entry: " + newUserDN);
-                final ChaiConfiguration chaiConfiguration = new ChaiConfiguration(chaiProvider.getChaiConfiguration());
-                chaiConfiguration.setSetting(ChaiSetting.BIND_DN, newUserDN);
-                chaiConfiguration.setSetting(ChaiSetting.BIND_PASSWORD, temporaryPassword.getStringValue());
-                final ChaiProvider bindAsProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
-                final ChaiUser bindAsUser = ChaiFactory.createChaiUser(newUserDN, bindAsProvider);
+                final ChaiConfiguration chaiConfiguration = ChaiConfiguration.builder(chaiProvider.getChaiConfiguration())
+                        .setSetting(ChaiSetting.BIND_DN, newUserDN)
+                        .setSetting(ChaiSetting.BIND_PASSWORD, temporaryPassword.getStringValue())
+                        .build();
+                final ChaiProvider bindAsProvider = pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(chaiConfiguration);
+                final ChaiUser bindAsUser = bindAsProvider.getEntryFactory().newChaiUser(newUserDN);
                 bindAsUser.changePassword(temporaryPassword.getStringValue(), userPassword.getStringValue());
                 NewUserUtils.LOGGER.debug(pwmSession, "changed to user requested password for new user entry: " + newUserDN);
                 bindAsProvider.close();
@@ -243,7 +243,7 @@ class NewUserUtils {
             }
 
             // add AD-specific attributes
-            if (ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY == chaiProvider.getDirectoryVendor()) {
+            if (DirectoryVendor.ACTIVE_DIRECTORY == chaiProvider.getDirectoryVendor()) {
                 try {
                     theUser.writeStringAttribute("userAccountControl", "512");
                 } catch (ChaiOperationException e) {
@@ -471,8 +471,17 @@ class NewUserUtils {
 
         switch (tokenType) {
             case SMS: {
-                final String toNum = tokenPayloadMap.get(pwmApplication.getConfig().getDefaultLdapProfile().readSettingAsString(PwmSetting.SMS_USER_PHONE_ATTRIBUTE));
-
+                String toNum = null;
+                final NewUserForm userForm = newUserBean.getNewUserForm();
+                if(userForm!=null && userForm.getFormData() != null && userForm.getFormData().get(pwmApplication.getConfig().getDefaultLdapProfile().readSettingAsString(PwmSetting.SMS_USER_PHONE_ATTRIBUTE))!=null)
+                {
+                    toNum = userForm.getFormData().get(pwmApplication.getConfig().getDefaultLdapProfile().readSettingAsString(PwmSetting.SMS_USER_PHONE_ATTRIBUTE));    
+                    if(toNum.isEmpty())
+                    {
+                        toNum=null;
+                    }
+                }
+                
                 final RestTokenDataClient.TokenDestinationData inputTokenDestData = new RestTokenDataClient.TokenDestinationData(
                         null, toNum, null);
                 final RestTokenDataClient restTokenDataClient = new RestTokenDataClient(pwmApplication);
@@ -481,7 +490,11 @@ class NewUserUtils {
                         inputTokenDestData,
                         null,
                         pwmRequest.getLocale());
-
+                if(outputDestTokenData == null || outputDestTokenData.getSms() == null || outputDestTokenData.getSms().isEmpty())
+                {
+                    //avoid sending SMS code token
+                    break;
+                }
                 final String tokenKey;
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
@@ -527,7 +540,11 @@ class NewUserUtils {
                         inputTokenDestData,
                         null,
                         pwmRequest.getLocale());
-
+                if(outputDestTokenData == null || outputDestTokenData.getEmail() == null || outputDestTokenData.getEmail().isEmpty())
+                {
+                    //avoid sending Email code token
+                    break;
+                }
                 final String tokenKey;
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(

+ 3 - 1
server/src/main/java/password/pwm/i18n/Display.java

@@ -117,9 +117,11 @@ public enum Display implements PwmDisplayBundle {
     Display_PasswordGeneration,
     Display_PasswordNoExpire,
     Display_PasswordPrompt,
+    Display_PasswordStrengthVeryHigh,
     Display_PasswordStrengthHigh,
-    Display_PasswordStrengthLow,
     Display_PasswordStrengthMedium,
+    Display_PasswordStrengthLow,
+    Display_PasswordStrengthVeryLow,
     Display_PasswordReplicationStatus,
     Display_PasswordWarn,
     Display_PeopleSearch,

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

@@ -23,10 +23,12 @@
 package password.pwm.ldap;
 
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.ChaiProviderFactory;
+import com.novell.ldapchai.provider.DirectoryVendor;
+import com.novell.ldapchai.provider.SearchScope;
 import com.novell.ldapchai.util.ChaiUtility;
 import com.novell.ldapchai.util.SearchHelper;
 import password.pwm.AppProperty;
@@ -50,11 +52,18 @@ import java.util.TreeMap;
 
 public class LdapBrowser {
     private static final PwmLogger LOGGER = PwmLogger.forClass(LdapBrowser.class);
-    final StoredConfigurationImpl storedConfiguration;
+    private final StoredConfigurationImpl storedConfiguration;
 
-    private Map<String,ChaiProvider> providerCache = new HashMap<>();
+    private final ChaiProviderFactory chaiProviderFactory ;
+    private final Map<String,ChaiProvider> providerCache = new HashMap<>();
 
-    public LdapBrowser(final StoredConfigurationImpl storedConfiguration) throws PwmUnrecoverableException {
+    public LdapBrowser(
+            final ChaiProviderFactory chaiProviderFactory,
+            final StoredConfigurationImpl storedConfiguration
+            )
+            throws PwmUnrecoverableException
+    {
+        this.chaiProviderFactory = chaiProviderFactory;
         this.storedConfiguration = storedConfiguration;
     }
 
@@ -106,7 +115,7 @@ public class LdapBrowser {
         if (adRootDNList(profileID).contains(dn)) {
             result.setParentDN("");
         } else if (dn != null && !dn.isEmpty()) {
-            final ChaiEntry dnEntry = ChaiFactory.createChaiEntry(dn, getChaiProvider(profileID));
+            final ChaiEntry dnEntry = getChaiProvider(profileID).getEntryFactory().newChaiEntry(dn);
             final ChaiEntry parentEntry = dnEntry.getParentEntry();
             if (parentEntry == null) {
                 result.setParentDN("");
@@ -122,7 +131,7 @@ public class LdapBrowser {
         if (!providerCache.containsKey(profile)) {
             final Configuration configuration = new Configuration(storedConfiguration);
             final LdapProfile ldapProfile = LdapProfile.makeFromStoredConfiguration(storedConfiguration, profile);
-            final ChaiProvider chaiProvider = LdapOperationsHelper.openProxyChaiProvider(null,ldapProfile,configuration,null);
+            final ChaiProvider chaiProvider = LdapOperationsHelper.openProxyChaiProvider(chaiProviderFactory, null,ldapProfile,configuration,null);
             providerCache.put(profile,chaiProvider);
         }
         return providerCache.get(profile);
@@ -150,7 +159,7 @@ public class LdapBrowser {
 
         final HashMap<String, Boolean> returnMap = new HashMap<>();
         final ChaiProvider chaiProvider = getChaiProvider(profile);
-        if ((dn == null || dn.isEmpty()) && chaiProvider.getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+        if ((dn == null || dn.isEmpty()) && chaiProvider.getDirectoryVendor() == DirectoryVendor.ACTIVE_DIRECTORY) {
             final Set<String> adRootDNList = adRootDNList(profile);
             for (final String rootDN : adRootDNList) {
                 returnMap.put(rootDN, true);
@@ -163,7 +172,7 @@ public class LdapBrowser {
                 searchHelper.setFilter("(objectclass=*)");
                 searchHelper.setMaxResults(getMaxSizeLimit());
                 searchHelper.setAttributes("subordinateCount");
-                searchHelper.setSearchScope(ChaiProvider.SEARCH_SCOPE.ONE);
+                searchHelper.setSearchScope(SearchScope.ONE);
                 results = chaiProvider.searchMultiValues(dn, searchHelper);
 
             }
@@ -180,7 +189,7 @@ public class LdapBrowser {
                     searchHelper.setFilter("(objectclass=*)");
                     searchHelper.setMaxResults(1);
                     searchHelper.setAttributes(Collections.emptyList());
-                    searchHelper.setSearchScope(ChaiProvider.SEARCH_SCOPE.ONE);
+                    searchHelper.setSearchScope(SearchScope.ONE);
                     try {
                         final Map<String, Map<String, String>> subSearchResults = chaiProvider.search(resultDN, searchHelper);
                         hasSubs = !subSearchResults.isEmpty();
@@ -197,7 +206,7 @@ public class LdapBrowser {
     private Set<String> adRootDNList(final String profile) throws ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException {
         final ChaiProvider chaiProvider = getChaiProvider(profile);
         final Set<String> adRootValues = new HashSet<>();
-        if (chaiProvider.getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+        if (chaiProvider.getDirectoryVendor() == DirectoryVendor.ACTIVE_DIRECTORY) {
             final ChaiEntry chaiEntry = ChaiUtility.getRootDSE(chaiProvider);
             adRootValues.addAll(chaiEntry.readMultiStringAttribute("namingContexts"));
         }

+ 49 - 18
server/src/main/java/password/pwm/ldap/LdapConnectionService.java

@@ -24,6 +24,7 @@ package password.pwm.ldap;
 
 import com.google.gson.reflect.TypeToken;
 import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.ChaiProviderFactory;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.config.option.DataStorageMethod;
@@ -39,10 +40,9 @@ import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
 
 import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -56,6 +56,7 @@ public class LdapConnectionService implements PwmService {
     private STATUS status = STATUS.NEW;
     private AtomicLoopIntIncrementer slotIncrementer;
     private final ThreadLocal<Map<LdapProfile,ChaiProvider>> threadLocalProvider = new ThreadLocal<>();
+    private ChaiProviderFactory chaiProviderFactory;
 
     public STATUS status()
     {
@@ -67,6 +68,8 @@ public class LdapConnectionService implements PwmService {
     {
         this.pwmApplication = pwmApplication;
 
+        chaiProviderFactory = ChaiProviderFactory.newProviderFactory();
+
         // read the lastLoginTime
         this.lastLdapErrors.putAll(readLastLdapFailure(pwmApplication));
 
@@ -85,9 +88,9 @@ public class LdapConnectionService implements PwmService {
     {
         status = STATUS.CLOSED;
         LOGGER.trace("closing ldap proxy connections");
-        for (final ChaiProvider existingProvider : getAllProviders()) {
+        if (chaiProviderFactory != null) {
             try {
-                existingProvider.close();
+                chaiProviderFactory.close();
             } catch (Exception e) {
                 LOGGER.error("error closing ldap proxy connection: " + e.getMessage(), e);
             }
@@ -102,7 +105,13 @@ public class LdapConnectionService implements PwmService {
 
     public ServiceInfoBean serviceInfo()
     {
-        return new ServiceInfoBean(Collections.singletonList(DataStorageMethod.LDAP));
+        final Map<String,String> debugProperties = new LinkedHashMap<>();
+        debugProperties.putAll(chaiProviderFactory.getGlobalStatistics());
+        debugProperties.putAll(connectionDebugInfo());
+        return new ServiceInfoBean(
+                Collections.singletonList(DataStorageMethod.LDAP),
+                Collections.unmodifiableMap(debugProperties)
+        );
     }
 
 
@@ -151,6 +160,7 @@ public class LdapConnectionService implements PwmService {
 
         try {
             final ChaiProvider newProvider = LdapOperationsHelper.openProxyChaiProvider(
+                    pwmApplication,
                     null,
                     ldapProfile,
                     pwmApplication.getConfig(),
@@ -230,25 +240,46 @@ public class LdapConnectionService implements PwmService {
         return perProfile;
     }
 
-    private Collection<ChaiProvider> getAllProviders() {
-        final List<ChaiProvider> returnList = new ArrayList<>();
-        for (final Map<Integer,ChaiProvider> loopProfileMap : proxyChaiProviders.values()) {
-            for (final ChaiProvider chaiProvider : loopProfileMap.values()) {
-                if (chaiProvider != null) {
-                    returnList.add(chaiProvider);
+    public int connectionCount() {
+        int count = 0;
+        if (chaiProviderFactory != null) {
+            for (final ChaiProvider chaiProvider : chaiProviderFactory.activeProviders()) {
+                if (chaiProvider.isConnected()) {
+                    count++;
                 }
             }
         }
-        return Collections.unmodifiableList(returnList);
+        return count;
     }
 
-    public int connectionCount() {
-        int count = 0;
-        for (final ChaiProvider chaiProvider : getAllProviders()) {
-            if (chaiProvider.isConnected()) {
-                count++;
+    public ChaiProviderFactory getChaiProviderFactory() {
+        return chaiProviderFactory;
+    }
+
+    private enum DebugKey {
+        ALLOCATED_CONNECTIONS,
+        ACTIVE_CONNECTIONS,
+        IDLE_CONNECTIONS,
+    }
+
+    private Map<String,String> connectionDebugInfo() {
+        int allocatedConnections = 0;
+        int activeConnections = 0;
+        int idleConnections = 0;
+        if (chaiProviderFactory != null) {
+            for (final ChaiProvider chaiProvider : chaiProviderFactory.activeProviders()) {
+                allocatedConnections++;
+                if (chaiProvider.isConnected()) {
+                    activeConnections++;
+                } else {
+                    idleConnections++;
+                }
             }
         }
-        return count;
+        final Map<String,String> debugInfo = new HashMap<>();
+        debugInfo.put(DebugKey.ALLOCATED_CONNECTIONS.name(), String.valueOf(allocatedConnections));
+        debugInfo.put(DebugKey.ACTIVE_CONNECTIONS.name(), String.valueOf(activeConnections));
+        debugInfo.put(DebugKey.IDLE_CONNECTIONS.name(), String.valueOf(idleConnections));
+        return Collections.unmodifiableMap(debugInfo);
     }
 }

+ 10 - 9
server/src/main/java/password/pwm/ldap/LdapDebugDataGenerator.java

@@ -23,14 +23,13 @@
 package password.pwm.ldap;
 
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
-import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
 import com.novell.ldapchai.util.ChaiUtility;
+import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -50,6 +49,7 @@ public class LdapDebugDataGenerator {
     private static final PwmLogger LOGGER = PwmLogger.forClass(LdapDebugDataGenerator.class);
 
     public static List<LdapDebugInfo> makeLdapDebugInfos(
+            final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
             final Configuration configuration,
             final Locale locale
@@ -63,6 +63,7 @@ public class LdapDebugDataGenerator {
             ldapDebugInfo.setDisplayName(ldapProfile.getDisplayName(locale));
             try {
                 final ChaiProvider chaiProvider = LdapOperationsHelper.createChaiProvider(
+                        pwmApplication,
                         null,
                         ldapProfile,
                         configuration,
@@ -74,7 +75,7 @@ public class LdapDebugDataGenerator {
                 for (final ChaiConfiguration chaiConfiguration : chaiConfigurations) {
                     final LdapDebugServerInfo ldapDebugServerInfo = new LdapDebugServerInfo();
                     ldapDebugServerInfo.setLdapServerlUrl(chaiConfiguration.getSetting(ChaiSetting.BIND_URLS));
-                    final ChaiProvider loopProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
+                    final ChaiProvider loopProvider = chaiProvider.getProviderFactory().newProvider(chaiConfiguration);
 
                     {
                         final ChaiEntry rootDSEentry = ChaiUtility.getRootDSE(loopProvider);
@@ -86,8 +87,8 @@ public class LdapDebugDataGenerator {
                         final String proxyUserDN = ldapProfile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN);
                         if (proxyUserDN != null) {
                             ldapDebugServerInfo.setProxyDN(proxyUserDN);
-                            final ChaiEntry proxyUserEntry = ChaiFactory.createChaiEntry(proxyUserDN, chaiProvider);
-                            if (proxyUserEntry.isValid()) {
+                            final ChaiEntry proxyUserEntry = chaiProvider.getEntryFactory().newChaiEntry(proxyUserDN);
+                            if (proxyUserEntry.exists()) {
                                 final Map<String, List<String>> proxyUserData = LdapOperationsHelper.readAllEntryAttributeValues(proxyUserEntry);
                                 ldapDebugServerInfo.setProxyUserAttributes(proxyUserData);
                             }
@@ -99,8 +100,8 @@ public class LdapDebugDataGenerator {
                         final String testUserDN = ldapProfile.readSettingAsString(PwmSetting.LDAP_TEST_USER_DN);
                         if (testUserDN != null) {
                             ldapDebugServerInfo.setTestUserDN(testUserDN);
-                            final ChaiEntry testUserEntry = ChaiFactory.createChaiEntry(testUserDN, chaiProvider);
-                            if (testUserEntry.isValid()) {
+                            final ChaiEntry testUserEntry = chaiProvider.getEntryFactory().newChaiEntry(testUserDN);
+                            if (testUserEntry.exists()) {
                                 final Map<String, List<String>> testUserdata = LdapOperationsHelper.readAllEntryAttributeValues(testUserEntry);
                                 ldapDebugServerInfo.setTestUserAttributes(testUserdata);
                             }
@@ -122,8 +123,8 @@ public class LdapDebugDataGenerator {
     private Map<String,List<String>> readUserAttributeData(final ChaiProvider chaiProvider, final String userDN)
             throws ChaiUnavailableException, ChaiOperationException
     {
-        final ChaiEntry testUserEntry = ChaiFactory.createChaiEntry(userDN, chaiProvider);
-        if (testUserEntry.isValid()) {
+        final ChaiEntry testUserEntry = chaiProvider.getEntryFactory().newChaiEntry(userDN);
+        if (testUserEntry.exists()) {
             final Map<String,List<String>> returnData = new LinkedHashMap<>();
             final Map<String, List<String>> testUserdata = LdapOperationsHelper.readAllEntryAttributeValues(testUserEntry);
             testUserdata.put("dn",Collections.singletonList(userDN));

+ 79 - 38
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -24,7 +24,6 @@ package password.pwm.ldap;
 
 import com.novell.ldapchai.ChaiConstant;
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.cr.Answer;
 import com.novell.ldapchai.exception.ChaiOperationException;
@@ -33,6 +32,7 @@ import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.SearchScope;
 import com.novell.ldapchai.util.SearchHelper;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
@@ -65,7 +65,6 @@ import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -91,7 +90,7 @@ public class LdapOperationsHelper {
             return;
         }
         final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
-        final ChaiUser theUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider);
+        final ChaiUser theUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
         addUserObjectClass(sessionLabel, theUser, newObjClasses);
     }
 
@@ -125,6 +124,25 @@ public class LdapOperationsHelper {
     }
 
     public static ChaiProvider openProxyChaiProvider(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final LdapProfile ldapProfile,
+            final Configuration config,
+            final StatisticsManager statisticsManager
+    )
+            throws PwmUnrecoverableException
+    {
+        return openProxyChaiProvider(
+                pwmApplication.getLdapConnectionService().getChaiProviderFactory(),
+                sessionLabel,
+                ldapProfile,
+                config,
+                statisticsManager
+        );
+    }
+
+    static ChaiProvider openProxyChaiProvider(
+            final ChaiProviderFactory chaiProviderFactory,
             final SessionLabel sessionLabel,
             final LdapProfile ldapProfile,
             final Configuration config,
@@ -138,7 +156,7 @@ public class LdapOperationsHelper {
         final PasswordData proxyPW = ldapProfile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD);
 
         try {
-            return createChaiProvider(sessionLabel, ldapProfile, config, proxyDN, proxyPW);
+            return createChaiProvider(chaiProviderFactory, sessionLabel, ldapProfile, config, proxyDN, proxyPW);
         } catch (ChaiUnavailableException e) {
             if (statisticsManager != null) {
                 statisticsManager.incrementValue(Statistic.LDAP_UNAVAILABLE_COUNT);
@@ -218,7 +236,6 @@ public class LdapOperationsHelper {
      * <p/>
      * Any ldap operation exceptions are not reported (but logged).
      *
-     * @param pwmSession       for looking up session info
      * @param theUser          User to write to
      * @param formValues       A map with {@link FormConfiguration} keys and String values.
      * @throws ChaiUnavailableException if the directory is unavailable
@@ -449,8 +466,28 @@ public class LdapOperationsHelper {
         }
     }
 
+    public static ChaiProvider createChaiProvider(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final LdapProfile ldapProfile,
+            final Configuration config,
+            final String userDN,
+            final PasswordData userPassword
+    )
+            throws ChaiUnavailableException, PwmUnrecoverableException
+    {
+        return createChaiProvider(
+                pwmApplication.getLdapConnectionService().getChaiProviderFactory(),
+                sessionLabel,
+                ldapProfile,
+                config,
+                userDN,
+                userPassword
+        );
+    }
 
     public static ChaiProvider createChaiProvider(
+            final ChaiProviderFactory chaiProviderFactory,
             final SessionLabel sessionLabel,
             final LdapProfile ldapProfile,
             final Configuration config,
@@ -462,10 +499,11 @@ public class LdapOperationsHelper {
         final List<String> ldapURLs = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_SERVER_URLS);
         final ChaiConfiguration chaiConfig = createChaiConfiguration(config, ldapProfile, ldapURLs, userDN, userPassword);
         LOGGER.trace(sessionLabel,"creating new ldap connection using config: " + chaiConfig.toString());
-        return ChaiProviderFactory.createProvider(chaiConfig);
+        return chaiProviderFactory.newProvider(chaiConfig);
     }
 
     public static ChaiProvider createChaiProvider(
+            final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
             final Configuration config,
             final LdapProfile ldapProfile,
@@ -477,7 +515,7 @@ public class LdapOperationsHelper {
     {
         final ChaiConfiguration chaiConfig = createChaiConfiguration( config, ldapProfile, ldapURLs, userDN, userPassword);
         LOGGER.trace(sessionLabel,"creating new ldap connection using config: " + chaiConfig.toString());
-        return ChaiProviderFactory.createProvider(chaiConfig);
+        return pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(chaiConfig);
     }
 
     public static ChaiConfiguration createChaiConfiguration(
@@ -501,32 +539,38 @@ public class LdapOperationsHelper {
     )
             throws PwmUnrecoverableException
     {
-        final ChaiConfiguration chaiConfig = new ChaiConfiguration(ldapURLs, userDN, userPassword == null ? null : userPassword.getStringValue());
+        final ChaiConfiguration.ChaiConfigurationBuilder configBuilder = ChaiConfiguration.builder(
+                ldapURLs,
+                userDN,
+                userPassword == null
+                        ? null
+                        : userPassword.getStringValue()
+        );
 
-        chaiConfig.setSetting(ChaiSetting.PROMISCUOUS_SSL, config.readAppProperty(AppProperty.LDAP_PROMISCUOUS_ENABLE));
+        configBuilder.setSetting(ChaiSetting.PROMISCUOUS_SSL, config.readAppProperty(AppProperty.LDAP_PROMISCUOUS_ENABLE));
         {
             final boolean enableNmasExtensions = Boolean.parseBoolean(config.readAppProperty(AppProperty.LDAP_EXTENSIONS_NMAS_ENABLE));
-            chaiConfig.setSetting(ChaiSetting.EDIRECTORY_ENABLE_NMAS, Boolean.toString(enableNmasExtensions));
+            configBuilder.setSetting(ChaiSetting.EDIRECTORY_ENABLE_NMAS, Boolean.toString(enableNmasExtensions));
         }
 
-        chaiConfig.setSetting(ChaiSetting.CR_CHAI_STORAGE_ATTRIBUTE, ldapProfile.readSettingAsString(PwmSetting.CHALLENGE_USER_ATTRIBUTE));
-        chaiConfig.setSetting(ChaiSetting.CR_ALLOW_DUPLICATE_RESPONSES, Boolean.toString(config.readSettingAsBoolean(PwmSetting.CHALLENGE_ALLOW_DUPLICATE_RESPONSES)));
-        chaiConfig.setSetting(ChaiSetting.CR_CASE_INSENSITIVE, Boolean.toString(config.readSettingAsBoolean(PwmSetting.CHALLENGE_CASE_INSENSITIVE)));
+        configBuilder.setSetting(ChaiSetting.CR_CHAI_STORAGE_ATTRIBUTE, ldapProfile.readSettingAsString(PwmSetting.CHALLENGE_USER_ATTRIBUTE));
+        configBuilder.setSetting(ChaiSetting.CR_ALLOW_DUPLICATE_RESPONSES, Boolean.toString(config.readSettingAsBoolean(PwmSetting.CHALLENGE_ALLOW_DUPLICATE_RESPONSES)));
+        configBuilder.setSetting(ChaiSetting.CR_CASE_INSENSITIVE, Boolean.toString(config.readSettingAsBoolean(PwmSetting.CHALLENGE_CASE_INSENSITIVE)));
         {
             final String setting = config.readAppProperty(AppProperty.SECURITY_RESPONSES_HASH_ITERATIONS);
             if (setting != null && setting.length() > 0) {
                 final int intValue = Integer.parseInt(setting);
-                chaiConfig.setSetting(ChaiSetting.CR_CHAI_SALT_COUNT, Integer.toString(intValue));
+                configBuilder.setSetting(ChaiSetting.CR_CHAI_SALT_COUNT, Integer.toString(intValue));
             }
         }
 
-        chaiConfig.setSetting(ChaiSetting.JNDI_ENABLE_POOL, "false"); // can cause issues with previous password authentication
+        configBuilder.setSetting(ChaiSetting.JNDI_ENABLE_POOL, "false"); // can cause issues with previous password authentication
 
-        chaiConfig.setSetting(ChaiSetting.CR_DEFAULT_FORMAT_TYPE, Answer.FormatType.SHA1_SALT.toString());
+        configBuilder.setSetting(ChaiSetting.CR_DEFAULT_FORMAT_TYPE, Answer.FormatType.SHA1_SALT.toString());
         final String storageMethodString = config.readSettingAsString(PwmSetting.CHALLENGE_STORAGE_HASHED);
         try {
             final Answer.FormatType formatType = Answer.FormatType.valueOf(storageMethodString);
-            chaiConfig.setSetting(ChaiSetting.CR_DEFAULT_FORMAT_TYPE, formatType.toString());
+            configBuilder.setSetting(ChaiSetting.CR_DEFAULT_FORMAT_TYPE, formatType.toString());
         } catch (Exception e) {
             LOGGER.warn("unknown CR storage format type '" + storageMethodString + "' ");
         }
@@ -534,27 +578,26 @@ public class LdapOperationsHelper {
         final List<X509Certificate> ldapServerCerts = ldapProfile.readSettingAsCertificate(PwmSetting.LDAP_SERVER_CERTS);
         if (ldapServerCerts != null && ldapServerCerts.size() > 0) {
             final X509TrustManager tm = new X509Utils.CertMatchingTrustManager(config, ldapServerCerts);
-            chaiConfig.setTrustManager(new X509TrustManager[]{tm});
+            configBuilder.setTrustManager(new X509TrustManager[]{tm});
         }
 
         final String idleTimeoutMsString = config.readAppProperty(AppProperty.LDAP_CONNECTION_TIMEOUT);
-        chaiConfig.setSetting(ChaiSetting.LDAP_CONNECT_TIMEOUT,idleTimeoutMsString);
+        configBuilder.setSetting(ChaiSetting.LDAP_CONNECT_TIMEOUT,idleTimeoutMsString);
 
         // set the watchdog idle timeout.
         final int idleTimeoutMs = (int)config.readSettingAsLong(PwmSetting.LDAP_IDLE_TIMEOUT) * 1000;
         if (idleTimeoutMs > 0) {
-            chaiConfig.setSetting(ChaiSetting.WATCHDOG_ENABLE, "true");
-            chaiConfig.setSetting(ChaiSetting.WATCHDOG_IDLE_TIMEOUT, idleTimeoutMsString);
-            chaiConfig.setSetting(ChaiSetting.WATCHDOG_CHECK_FREQUENCY, Long.toString(5 * 1000));
+            configBuilder.setSetting(ChaiSetting.WATCHDOG_ENABLE, "true");
+            configBuilder.setSetting(ChaiSetting.WATCHDOG_IDLE_TIMEOUT, idleTimeoutMsString);
         } else {
-            chaiConfig.setSetting(ChaiSetting.WATCHDOG_ENABLE, "false");
+            configBuilder.setSetting(ChaiSetting.WATCHDOG_ENABLE, "false");
         }
 
-        chaiConfig.setSetting(ChaiSetting.LDAP_SEARCH_PAGING_ENABLE, config.readAppProperty(AppProperty.LDAP_SEARCH_PAGING_ENABLE));
-        chaiConfig.setSetting(ChaiSetting.LDAP_SEARCH_PAGING_SIZE, config.readAppProperty(AppProperty.LDAP_SEARCH_PAGING_SIZE));
+        configBuilder.setSetting(ChaiSetting.LDAP_SEARCH_PAGING_ENABLE, config.readAppProperty(AppProperty.LDAP_SEARCH_PAGING_ENABLE));
+        configBuilder.setSetting(ChaiSetting.LDAP_SEARCH_PAGING_SIZE, config.readAppProperty(AppProperty.LDAP_SEARCH_PAGING_SIZE));
 
         if (config.readSettingAsBoolean(PwmSetting.AD_ENFORCE_PW_HISTORY_ON_SET)) {
-            chaiConfig.setSetting(ChaiSetting.AD_SET_POLICY_HINTS_ON_PW_SET,"true");
+            configBuilder.setSetting(ChaiSetting.AD_SET_POLICY_HINTS_ON_PW_SET,"true");
         }
 
         // write out any configured values;
@@ -568,20 +611,20 @@ public class LdapOperationsHelper {
                 if (theSetting == null) {
                     LOGGER.warn("ignoring unknown chai setting '" + key + "'");
                 } else {
-                    chaiConfig.setSetting(theSetting, entry.getValue());
+                    configBuilder.setSetting(theSetting, entry.getValue());
                 }
             }
         }
 
         // set ldap referrals
-        chaiConfig.setSetting(ChaiSetting.LDAP_FOLLOW_REFERRALS,String.valueOf(config.readSettingAsBoolean(PwmSetting.LDAP_FOLLOW_REFERRALS)));
+        configBuilder.setSetting(ChaiSetting.LDAP_FOLLOW_REFERRALS,String.valueOf(config.readSettingAsBoolean(PwmSetting.LDAP_FOLLOW_REFERRALS)));
 
         // enable wire trace;
         if (config.readSettingAsBoolean(PwmSetting.LDAP_ENABLE_WIRE_TRACE)) {
-            chaiConfig.setSetting(ChaiSetting.WIRETRACE_ENABLE, "true");
+            configBuilder.setSetting(ChaiSetting.WIRETRACE_ENABLE, "true");
         }
 
-        return chaiConfig;
+        return configBuilder.build();
     }
 
     /**
@@ -608,7 +651,7 @@ public class LdapOperationsHelper {
 
         if (updateAttribute != null && updateAttribute.length() > 0) {
             try {
-                theUser.writeDateAttribute(updateAttribute, new Date());
+                theUser.writeDateAttribute(updateAttribute, Instant.now());
                 LOGGER.debug(sessionLabel, "wrote pwdLastModified update attribute for " + theUser.getEntryDN());
                 success = true;
             } catch (ChaiOperationException e) {
@@ -623,7 +666,7 @@ public class LdapOperationsHelper {
             throws ChaiUnavailableException, ChaiOperationException
     {
         final SearchHelper searchHelper = new SearchHelper();
-        searchHelper.setSearchScope(ChaiProvider.SEARCH_SCOPE.BASE);
+        searchHelper.setSearchScope(SearchScope.BASE);
         searchHelper.setFilter("(objectClass=*)");
         final Map<String, Map<String, List<String>>> results = chaiEntry.getChaiProvider().searchMultiValues(chaiEntry.getEntryDN(), searchHelper);
         if (!results.isEmpty()) {
@@ -688,14 +731,12 @@ public class LdapOperationsHelper {
 
     public static Instant readPasswordExpirationTime(final ChaiUser theUser) {
         try {
-            Date ldapPasswordExpirationTime = theUser.readPasswordExpirationDate();
-            if (ldapPasswordExpirationTime != null && ldapPasswordExpirationTime.getTime() < 0) {
+            Instant ldapPasswordExpirationTime = theUser.readPasswordExpirationDate();
+            if (ldapPasswordExpirationTime != null && ldapPasswordExpirationTime.toEpochMilli() < 0) {
                 // If ldapPasswordExpirationTime is less than 0, this may indicate an extremely late date, past the epoch.
                 ldapPasswordExpirationTime = null;
             }
-            return ldapPasswordExpirationTime == null
-                    ? null
-                    : ldapPasswordExpirationTime.toInstant();
+            return ldapPasswordExpirationTime;
         } catch (Exception e) {
             LOGGER.warn("error reading password expiration time: " + e.getMessage());
         }
@@ -715,7 +756,7 @@ public class LdapOperationsHelper {
         }
 
         final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
-        final ChaiUser chaiUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider);
+        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
 
         // use chai (nmas) to retrieve user password
         if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.EDIRECTORY_READ_USER_PWD)) {

+ 8 - 4
server/src/main/java/password/pwm/ldap/LdapPermissionTester.java

@@ -24,14 +24,14 @@ package password.pwm.ldap;
 
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiException;
-import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.SearchScope;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -137,7 +137,11 @@ public class LdapPermissionTester {
             try {
                 LOGGER.trace(pwmSession, "checking ldap to see if " + userIdentity + " matches group '" + groupDN + "' using filter '" + filterString + "'");
                 final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
-                final Map<String, Map<String, String>> results = theUser.getChaiProvider().search(theUser.getEntryDN(), filterString, Collections.<String>emptySet(), ChaiProvider.SEARCH_SCOPE.BASE);
+                final Map<String, Map<String, String>> results = theUser.getChaiProvider().search(
+                        theUser.getEntryDN(),
+                        filterString,
+                        Collections.<String>emptySet(), SearchScope.BASE
+                );
                 if (results.size() == 1 && results.keySet().contains(theUser.getEntryDN())) {
                     result = true;
                 }
@@ -182,7 +186,7 @@ public class LdapPermissionTester {
             try {
                 LOGGER.trace(pwmSession, "checking ldap to see if " + userIdentity + " matches '" + filterString + "'");
                 final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
-                final Map<String, Map<String, String>> results = theUser.getChaiProvider().search(theUser.getEntryDN(), filterString, Collections.<String>emptySet(), ChaiProvider.SEARCH_SCOPE.BASE);
+                final Map<String, Map<String, String>> results = theUser.getChaiProvider().search(theUser.getEntryDN(), filterString, Collections.emptySet(), SearchScope.BASE);
                 if (results.size() == 1 && results.keySet().contains(theUser.getEntryDN())) {
                     result = true;
                 }

+ 7 - 2
server/src/main/java/password/pwm/ldap/UserInfo.java

@@ -33,7 +33,6 @@ import password.pwm.util.operations.otp.OTPUserRecord;
 
 import java.time.Instant;
 import java.util.Collection;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -66,6 +65,12 @@ public interface UserInfo {
 
     boolean isRequiresInteraction() throws PwmUnrecoverableException;
 
+    boolean isAccountEnabled() throws PwmUnrecoverableException;
+
+    boolean isAccountExpired() throws PwmUnrecoverableException;
+
+    boolean isPasswordLocked() throws PwmUnrecoverableException;
+
     Instant getPasswordLastModifiedTime() throws PwmUnrecoverableException;
 
     String getUserEmailAddress() throws PwmUnrecoverableException;
@@ -84,7 +89,7 @@ public interface UserInfo {
 
     String readStringAttribute(String attribute) throws PwmUnrecoverableException;
 
-    Date readDateAttribute(String attribute) throws PwmUnrecoverableException;
+    Instant readDateAttribute(String attribute) throws PwmUnrecoverableException;
 
     List<String> readMultiStringAttribute(String attribute) throws PwmUnrecoverableException;
 

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

@@ -37,7 +37,6 @@ import password.pwm.util.operations.otp.OTPUserRecord;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -80,6 +79,10 @@ public class UserInfoBean implements UserInfo {
     private final Instant lastLdapLoginTime;
     private final Instant accountExpirationTime;
 
+    private final boolean accountEnabled;
+    private final boolean accountExpired;
+    private final boolean passwordLocked;
+
     private final boolean requiresNewPassword;
     private final boolean requiresResponseConfig;
     private final boolean requiresOtpConfig;
@@ -96,10 +99,10 @@ public class UserInfoBean implements UserInfo {
     }
 
     @Override
-    public Date readDateAttribute(final String attribute) throws PwmUnrecoverableException
+    public Instant readDateAttribute(final String attribute) throws PwmUnrecoverableException
     {
         if (attributes.containsKey(attribute)) {
-            return EdirEntries.convertZuluToDate(attributes.get(attribute));
+            return EdirEntries.convertZuluToInstant(attributes.get(attribute));
         }
         return null;
     }

+ 33 - 13
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -22,12 +22,12 @@
 
 package password.pwm.ldap;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.SearchScope;
 import password.pwm.PwmApplication;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.bean.ResponseInfoBean;
@@ -66,7 +66,6 @@ import password.pwm.util.operations.otp.OTPUserRecord;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -108,7 +107,7 @@ public class UserInfoReader implements UserInfo {
         this.sessionLabel = sessionLabel;
 
         final ChaiProvider cachingProvider = CachingProxyWrapper.create(ChaiProvider.class, chaiProvider);
-        this.chaiUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), cachingProvider);
+        this.chaiUser = cachingProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
     }
 
     static UserInfo create(
@@ -153,10 +152,7 @@ public class UserInfoReader implements UserInfo {
     public Instant getLastLdapLoginTime() throws PwmUnrecoverableException
     {
         try {
-            final Date lastLoginTime = chaiUser.readLastLoginTime();
-            return lastLoginTime == null
-                    ? null
-                    : lastLoginTime.toInstant();
+            return chaiUser.readLastLoginTime();
         } catch (ChaiOperationException e) {
             LOGGER.warn(sessionLabel, "error reading user's last ldap login time: " + e.getMessage());
         } catch (ChaiUnavailableException e) {
@@ -327,6 +323,33 @@ public class UserInfoReader implements UserInfo {
         return false;
     }
 
+    @Override
+    public boolean isAccountEnabled() throws PwmUnrecoverableException {
+        try {
+            return chaiUser.isAccountEnabled();
+        } catch (ChaiException e) {
+            throw PwmUnrecoverableException.fromChaiException(e);
+        }
+    }
+
+    @Override
+    public boolean isAccountExpired() throws PwmUnrecoverableException {
+        try {
+            return chaiUser.isAccountExpired();
+        } catch (ChaiException e) {
+            throw PwmUnrecoverableException.fromChaiException(e);
+        }
+    }
+
+    @Override
+    public boolean isPasswordLocked() throws PwmUnrecoverableException {
+        try {
+            return chaiUser.isPasswordLocked();
+        } catch (ChaiException e) {
+            throw PwmUnrecoverableException.fromChaiException(e);
+        }
+    }
+
     @Override
     public boolean isRequiresResponseConfig() throws PwmUnrecoverableException
     {
@@ -491,10 +514,7 @@ public class UserInfoReader implements UserInfo {
     public Instant getAccountExpirationTime() throws PwmUnrecoverableException
     {
         try {
-            final Date accountExpireDate = chaiUser.readAccountExpirationDate();
-            return accountExpireDate == null
-                    ? null
-                    : accountExpireDate.toInstant();
+            return chaiUser.readAccountExpirationDate();
         } catch (ChaiOperationException e) {
             LOGGER.warn(sessionLabel, "error reading user's account expiration time: " + e.getMessage());
         } catch (ChaiUnavailableException e) {
@@ -554,7 +574,7 @@ public class UserInfoReader implements UserInfo {
     }
 
     @Override
-    public Date readDateAttribute(final String attribute)
+    public Instant readDateAttribute(final String attribute)
             throws PwmUnrecoverableException
     {
         try {
@@ -610,7 +630,7 @@ public class UserInfoReader implements UserInfo {
                         chaiUser.getEntryDN(),
                         "(objectclass=*)",
                         uncachedAttributes,
-                        ChaiProvider.SEARCH_SCOPE.BASE
+                        SearchScope.BASE
                 );
             } catch (ChaiOperationException e) {
                 final String msg = "ldap operational error while reading user data" + e.getMessage();

+ 313 - 0
server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java

@@ -0,0 +1,313 @@
+package password.pwm.ldap;
+
+import password.pwm.bean.LocalSessionStateBean;
+import password.pwm.bean.ResponseInfoBean;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.ViewStatusFields;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.DisplayElement;
+import password.pwm.i18n.Display;
+import password.pwm.util.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public final class ViewableUserInfoDisplayReader {
+    private ViewableUserInfoDisplayReader() {
+    }
+
+    public static List<DisplayElement> makeDisplayData(
+            final Set<ViewStatusFields> viewStatusFields,
+            final Configuration config,
+            final UserInfo userInfo,
+            final LocalSessionStateBean localSessionStateBean,
+            final Locale locale
+    )
+            throws PwmUnrecoverableException {
+        final List<DisplayElement> accountInfo = new ArrayList<>();
+        final DataElementMaker maker = new DataElementMaker(viewStatusFields, config, locale, accountInfo);
+
+        maker.add(
+                ViewStatusFields.Username,
+                Display.Field_Username,
+                userInfo.getUsername()
+        );
+
+        maker.add(
+                ViewStatusFields.UserDN,
+                Display.Field_UserDN,
+                userInfo.getUserIdentity().getUserDN()
+        );
+
+        if (config.getLdapProfiles().size() > 1) {
+            final String ldapProfileID = userInfo.getUserIdentity().getLdapProfileID();
+            final String value = config.getLdapProfiles().get(ldapProfileID).getDisplayName(locale);
+            maker.add(
+                    ViewStatusFields.UserDN,
+                    Display.Field_LdapProfile,
+                    value
+            );
+        }
+
+        maker.add(
+                ViewStatusFields.UserEmail,
+                Display.Field_UserEmail,
+                userInfo.getUserEmailAddress()
+        );
+
+        maker.add(
+                ViewStatusFields.UserSMS,
+                Display.Field_UserSMS,
+                userInfo.getUserSmsNumber()
+        );
+
+        maker.add(
+                ViewStatusFields.AccountEnabled,
+                Display.Field_AccountEnabled,
+                userInfo.isAccountEnabled()
+        );
+
+        maker.add(
+                ViewStatusFields.AccountExpired,
+                Display.Field_AccountExpired,
+                userInfo.isAccountExpired()
+        );
+
+        maker.add(
+                ViewStatusFields.GUID,
+                Display.Field_UserGUID,
+                userInfo.getUserGuid()
+        );
+
+        maker.add(
+                ViewStatusFields.AccountExpirationTime,
+                Display.Field_AccountExpirationTime,
+                userInfo.getAccountExpirationTime()
+        );
+
+        {
+            final Instant lastLoginTime = userInfo.getLastLdapLoginTime();
+            maker.add(
+                    ViewStatusFields.LastLoginTime,
+                    Display.Field_LastLoginTime,
+                    lastLoginTime
+            );
+
+            if (lastLoginTime != null) {
+                maker.add(
+                        ViewStatusFields.LastLoginTimeDelta,
+                        Display.Field_LastLoginTimeDelta,
+                        TimeDuration.fromCurrent(lastLoginTime).asLongString(locale)
+                );
+            }
+        }
+
+        maker.add(
+                ViewStatusFields.PasswordExpired,
+                Display.Field_PasswordExpired,
+                userInfo.getPasswordStatus().isExpired()
+        );
+
+        maker.add(
+                ViewStatusFields.PasswordPreExpired,
+                Display.Field_PasswordPreExpired,
+                userInfo.getPasswordStatus().isPreExpired()
+        );
+
+        maker.add(
+                ViewStatusFields.PasswordWarnPeriod,
+                Display.Field_PasswordWithinWarningPeriod,
+                userInfo.getPasswordStatus().isWarnPeriod()
+        );
+
+        maker.add(
+                ViewStatusFields.PasswordViolatesPolicy,
+                Display.Field_PasswordViolatesPolicy,
+                userInfo.getPasswordStatus().isViolatesPolicy()
+        );
+
+        maker.add(
+                ViewStatusFields.PasswordSetTime,
+                Display.Field_PasswordSetTime,
+                userInfo.getPasswordLastModifiedTime()
+        );
+
+        {
+            final String value = userInfo.getPasswordLastModifiedTime() != null
+                    ? TimeDuration.fromCurrent(userInfo.getPasswordLastModifiedTime()).asLongString(locale)
+                    : LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, config);
+            maker.add(
+                    ViewStatusFields.PasswordSetTimeDelta,
+                    Display.Field_PasswordSetTimeDelta,
+                    value
+            );
+        }
+
+        maker.add(
+                ViewStatusFields.PasswordExpireTime,
+                Display.Field_PasswordExpirationTime,
+                userInfo.getPasswordExpirationTime()
+        );
+
+        maker.add(
+                ViewStatusFields.IntruderDetect,
+                Display.Field_PasswordLocked,
+                userInfo.isPasswordLocked()
+        );
+
+        {
+            final ResponseInfoBean responseInfoBean = userInfo.getResponseInfoBean();
+            maker.add(
+                    ViewStatusFields.ResponsesStored,
+                    Display.Field_ResponsesStored,
+                    responseInfoBean != null
+            );
+
+            maker.add(
+                    ViewStatusFields.ResponsesNeeded,
+                    Display.Field_ResponsesNeeded,
+                    userInfo.isRequiresResponseConfig()
+            );
+
+
+            if (responseInfoBean != null) {
+                maker.add(
+                        ViewStatusFields.ResponsesTimestamp,
+                        Display.Field_ResponsesTimestamp,
+                        responseInfoBean.getTimestamp()
+                );
+            }
+        }
+
+        if (userInfo.getResponseInfoBean() != null) {
+            maker.add(
+                    ViewStatusFields.ResponsesTimestamp,
+                    Display.Field_ResponsesTimestamp,
+                    userInfo.getResponseInfoBean().getTimestamp()
+            );
+        }
+
+        if (config.readSettingAsBoolean(PwmSetting.OTP_ENABLED)) {
+            maker.add(
+                    ViewStatusFields.OTPStored,
+                    Display.Field_OTP_Stored,
+                    userInfo.getOtpUserRecord() != null
+            );
+
+            if (userInfo.getOtpUserRecord() != null) {
+                maker.add(
+                        ViewStatusFields.OTPTimestamp,
+                        Display.Field_OTP_Timestamp,
+                        userInfo.getOtpUserRecord().getTimestamp()
+                );
+            }
+        }
+
+        if (localSessionStateBean != null) {
+            maker.add(
+                    ViewStatusFields.NetworkAddress,
+                    Display.Field_NetworkAddress,
+                    localSessionStateBean.getSrcAddress()
+            );
+
+            maker.add(
+                    ViewStatusFields.NetworkHost,
+                    Display.Field_NetworkHost,
+                    localSessionStateBean.getSrcHostname()
+            );
+
+            {
+                final String value = localSessionStateBean.getLogoutURL() == null
+                        ? config.readSettingAsString(PwmSetting.URL_LOGOUT)
+                        : localSessionStateBean.getLogoutURL();
+
+                maker.add(
+                        ViewStatusFields.LogoutURL,
+                        Display.Field_LogoutURL,
+                        value
+                );
+            }
+
+            {
+                final String value = localSessionStateBean.getForwardURL() == null
+                        ? config.readSettingAsString(PwmSetting.URL_FORWARD)
+                        : localSessionStateBean.getForwardURL();
+
+                maker.add(
+                        ViewStatusFields.ForwardURL,
+                        Display.Field_ForwardURL,
+                        value
+                );
+            }
+        }
+
+
+        return Collections.unmodifiableList(accountInfo);
+    }
+
+    private static class DataElementMaker {
+        private final Configuration config;
+        private final Set<ViewStatusFields> viewStatusFields;
+        private final Locale locale;
+        private final List<DisplayElement> list;
+
+        DataElementMaker(final Set<ViewStatusFields> viewStatusFields, final Configuration config, final Locale locale, final List<DisplayElement> list) {
+            this.config = config;
+            this.viewStatusFields = viewStatusFields;
+            this.locale = locale;
+            this.list = list;
+        }
+
+        void add(final ViewStatusFields viewStatusField, final Display display, final Instant instant) {
+
+            if (!viewStatusFields.contains(viewStatusField)) {
+                return;
+            }
+
+            final String strValue = instant == null
+                    ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, config)
+                    : JavaHelper.toIsoDate(instant);
+
+            list.add(new DisplayElement(
+                    display.name(),
+                    DisplayElement.Type.timestamp,
+                    LocaleHelper.getLocalizedMessage(locale, display, config),
+                    strValue
+            ));
+        }
+
+        void add(final ViewStatusFields viewStatusField, final Display display, final boolean value) {
+            add(viewStatusField, display, LocaleHelper.booleanString(
+                    value,
+                    locale,
+                    config
+            ));
+        }
+
+        void add(final ViewStatusFields viewStatusField, final Display display, final String value) {
+
+            if (!viewStatusFields.contains(viewStatusField)) {
+                return;
+            }
+
+            final String strValue = StringUtil.isEmpty(value)
+                    ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, config)
+                    : value;
+
+            list.add(new DisplayElement(
+                    display.name(),
+                    DisplayElement.Type.string,
+                    LocaleHelper.getLocalizedMessage(locale, display, config),
+                    strValue
+            ));
+        }
+    }
+}

+ 17 - 15
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -23,7 +23,6 @@
 package password.pwm.ldap.auth;
 
 import com.novell.ldapchai.ChaiConstant;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiError;
 import com.novell.ldapchai.exception.ChaiException;
@@ -33,6 +32,7 @@ import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
 import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -64,6 +64,7 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
 
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
@@ -181,6 +182,9 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
         intruderManager.convenience().checkUserIdentity(userIdentity);
         intruderManager.check(RecordType.ADDRESS, sessionLabel.getSrcAddress());
 
+        // verify user is not account disabled
+        AuthenticationUtility.checkIfUserEligibleToAuthentication(pwmApplication, userIdentity);
+
         boolean allowBindAsUser = true;
         if (strategy == AuthenticationStrategy.ADMIN_PROXY) {
             allowBindAsUser = false;
@@ -191,17 +195,17 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                 testCredentials(userIdentity, password);
             } catch (PwmOperationalException e) {
                 boolean permitAuthDespiteError = false;
-                final ChaiProvider.DIRECTORY_VENDOR vendor = pwmApplication.getProxyChaiProvider(
+                final DirectoryVendor vendor = pwmApplication.getProxyChaiProvider(
                         userIdentity.getLdapProfileID()).getDirectoryVendor();
                 if (PwmError.PASSWORD_NEW_PASSWORD_REQUIRED == e.getError()) {
-                    if (vendor == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+                    if (vendor == DirectoryVendor.ACTIVE_DIRECTORY) {
                         if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_REQUIRE_NEW_PWD)) {
                             log(PwmLogLevel.INFO,
                                     "auth bind failed, but will allow login due to 'must change password on next login AD error', error: " + e.getErrorInformation().toDebugStr());
                             allowBindAsUser = false;
                             permitAuthDespiteError = true;
                         }
-                    } else if (vendor == ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS) {
+                    } else if (vendor == DirectoryVendor.ORACLE_DS) {
                         if (pwmApplication.getConfig().readSettingAsBoolean(
                                 PwmSetting.ORACLE_DS_ALLOW_AUTH_REQUIRE_NEW_PWD)) {
                             log(PwmLogLevel.INFO,
@@ -211,7 +215,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                         }
                     }
                 } else if (PwmError.PASSWORD_EXPIRED == e.getError()) { // handle ad case where password is expired
-                    if (vendor == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+                    if (vendor == DirectoryVendor.ACTIVE_DIRECTORY) {
                         if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_REQUIRE_NEW_PWD)) {
                             if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_EXPIRED)) {
                                 throw e;
@@ -229,9 +233,6 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                     throw e;
                 }
             }
-        } else {
-            // verify user is not account disabled
-            AuthenticationUtility.checkIfUserEligibleToAuthentication(pwmApplication, userIdentity);
         }
 
         statisticsManager.incrementValue(Statistic.AUTHENTICATIONS);
@@ -316,6 +317,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
         try {
             //read a provider using the user's DN and password.
             userProvider = LdapOperationsHelper.createChaiProvider(
+                    pwmApplication,
                     sessionLabel,
                     userIdentity.getLdapProfile(pwmApplication.getConfig()),
                     pwmApplication.getConfig(),
@@ -370,7 +372,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
         final boolean configAlwaysUseProxy = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_USE_PROXY_FOR_FORGOTTEN);
 
         final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
-        final ChaiUser chaiUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider);
+        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
 
         // try setting a random password on the account to authenticate.
         if (!configAlwaysUseProxy && requestedAuthType == AuthenticationType.AUTH_FROM_PUBLIC_MODULE) {
@@ -423,7 +425,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
             return null;
         }
 
-        if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) {
+        if (DirectoryVendor.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) {
             return null;
         }
 
@@ -435,11 +437,11 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
 
 
         if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) {
-            final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
+            final Instant date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
 
             final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME);
             if (enforceFromForgotten) {
-                if (new Date().before(date)) {
+                if (Instant.now().isBefore(date)) {
                     final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(
                             date);
                     throw new PwmUnrecoverableException(
@@ -463,7 +465,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
         }
 
         // oracle DS special case: passwordAllowChangeTime handler
-        if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) {
+        if (DirectoryVendor.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) {
             return;
         }
 
@@ -483,7 +485,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                 final boolean PostTempUseCurrentTime = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME));
                 if (PostTempUseCurrentTime) {
                     log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will replace with current time value");
-                    final String newTimeValue = OracleDSEntries.convertDateToZulu(new Date());
+                    final String newTimeValue = OracleDSEntries.convertDateToZulu(Instant.now());
                     final Set<String> values = new HashSet<>(Collections.singletonList(newTimeValue));
                     chaiProvider.writeStringAttribute(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME, values, true);
                     log(PwmLogLevel.TRACE, "wrote attribute value '" + newTimeValue + "' for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN());
@@ -521,7 +523,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
         final LdapProfile profile = pwmApplication.getConfig().getLdapProfiles().get(userIdentity.getLdapProfileID());
         final String proxyDN = profile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN);
         final PasswordData proxyPassword = profile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD);
-        return LdapOperationsHelper.createChaiProvider(sessionLabel, profile, pwmApplication.getConfig(), proxyDN, proxyPassword);
+        return LdapOperationsHelper.createChaiProvider(pwmApplication, sessionLabel, profile, pwmApplication.getConfig(), proxyDN, proxyPassword);
     }
 
     private void log(final PwmLogLevel level, final CharSequence message) {

+ 1 - 0
server/src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java

@@ -248,6 +248,7 @@ public class SessionAuthenticator {
             //read a provider using the user's DN and password.
 
             provider = LdapOperationsHelper.createChaiProvider(
+                    pwmApplication,
                     sessionLabel,
                     userIdentity.getLdapProfile(pwmApplication.getConfig()),
                     pwmApplication.getConfig(),

+ 2 - 3
server/src/main/java/password/pwm/ldap/schema/EdirSchemaExtender.java

@@ -24,7 +24,6 @@ package password.pwm.ldap.schema;
 
 import com.novell.ldap.client.SchemaParser;
 import com.novell.ldapchai.ChaiEntry;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -52,11 +51,11 @@ public class EdirSchemaExtender implements SchemaExtender {
     private ChaiEntry schemaEntry;
 
     private final StringBuilder activityLog = new StringBuilder();
-    private final Map<String,SchemaDefinition.State> stateMap = new HashMap();
+    private final Map<String,SchemaDefinition.State> stateMap = new HashMap<>();
 
     public void init(final ChaiProvider chaiProvider) throws PwmUnrecoverableException {
         try {
-            schemaEntry = ChaiFactory.createChaiEntry(LDAP_SCHEMA_DN, chaiProvider);
+            schemaEntry = chaiProvider.getEntryFactory().newChaiEntry(LDAP_SCHEMA_DN);
         } catch (ChaiUnavailableException e) {
             throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, e.getMessage()));
         }

+ 6 - 5
server/src/main/java/password/pwm/ldap/schema/SchemaManager.java

@@ -24,6 +24,7 @@ package password.pwm.ldap.schema;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -37,11 +38,11 @@ import java.util.Map;
 public class SchemaManager {
     private static final PwmLogger LOGGER = PwmLogger.forClass(SchemaManager.class);
 
-    private static final Map<ChaiProvider.DIRECTORY_VENDOR, Class<? extends SchemaExtender>> IMPLEMENTATIONS;
+    private static final Map<DirectoryVendor, Class<? extends SchemaExtender>> IMPLEMENTATIONS;
 
     static {
-        final Map<ChaiProvider.DIRECTORY_VENDOR, Class<? extends SchemaExtender>> implMap = new HashMap<>();
-        implMap.put(ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY, EdirSchemaExtender.class);
+        final Map<DirectoryVendor, Class<? extends SchemaExtender>> implMap = new HashMap<>();
+        implMap.put(DirectoryVendor.EDIRECTORY, EdirSchemaExtender.class);
         IMPLEMENTATIONS = Collections.unmodifiableMap(implMap);
     }
 
@@ -50,7 +51,7 @@ public class SchemaManager {
             throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, "provider is not connected"));
         }
         try {
-            if (chaiProvider.getDirectoryVendor() != ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+            if (chaiProvider.getDirectoryVendor() != DirectoryVendor.EDIRECTORY) {
                 throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, "directory vendor is not supported"));
             }
             final List<String> urls = chaiProvider.getChaiConfiguration().bindURLsAsList();
@@ -58,7 +59,7 @@ public class SchemaManager {
                 throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, "provider used for schema extension must have only a single ldap url defined"));
             }
 
-            final ChaiProvider.DIRECTORY_VENDOR vendor = chaiProvider.getDirectoryVendor();
+            final DirectoryVendor vendor = chaiProvider.getDirectoryVendor();
             final Class<? extends SchemaExtender> implClass = IMPLEMENTATIONS.get(vendor);
             final SchemaExtender schemaExtenderImpl = implClass.newInstance();
             schemaExtenderImpl.init(chaiProvider);

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

@@ -22,7 +22,6 @@
 
 package password.pwm.ldap.search;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
@@ -149,7 +148,7 @@ public class UserSearchEngine implements PwmService {
             if (inputIdentity != null) {
                 try {
                     final ChaiUser theUser = pwmApplication.getProxiedChaiUser(inputIdentity);
-                    if (theUser.isValid()) {
+                    if (theUser.exists()) {
                         final String canonicalDN;
                         canonicalDN = theUser.readCanonicalDN();
                         return new UserIdentity(canonicalDN, inputIdentity.getLdapProfileID());
@@ -525,8 +524,8 @@ public class UserSearchEngine implements PwmService {
         final Collection<LdapProfile> ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values();
         for (final LdapProfile ldapProfile : ldapProfiles) {
             final ChaiProvider provider = pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier());
-            final ChaiUser user = ChaiFactory.createChaiUser(userDN, provider);
-            if (user.isValid()) {
+            final ChaiUser user = provider.getEntryFactory().newChaiUser(userDN);
+            if (user.exists()) {
                 try {
                     return new UserIdentity(user.readCanonicalDN(), ldapProfile.getIdentifier());
                 } catch (ChaiOperationException e) {

+ 1 - 0
server/src/main/java/password/pwm/svc/PwmServiceEnum.java

@@ -29,6 +29,7 @@ import java.util.Collections;
 import java.util.List;
 
 public enum PwmServiceEnum {
+    LocalDBService(         password.pwm.util.localdb.LocalDBService.class,         Flag.StartDuringRuntimeInstance),
     SecureService(          password.pwm.util.secure.SecureService.class,           Flag.StartDuringRuntimeInstance),
     LdapConnectionService(  password.pwm.ldap.LdapConnectionService.class,          Flag.StartDuringRuntimeInstance),
     DatabaseService(        password.pwm.util.db.DatabaseService.class,             Flag.StartDuringRuntimeInstance),

+ 0 - 22
server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java

@@ -130,28 +130,6 @@ public class SessionTrackService implements PwmService {
         return Collections.emptyMap();
     }
 
-    public int ldapConnectionCount() {
-        int counter = 0;
-        try {
-            for (final String identifer : pwmApplication.getConfig().getLdapProfiles().keySet()) {
-                if (pwmApplication.getProxyChaiProvider(identifer).isConnected()) {
-                    counter++;
-                }
-            }
-
-            for (final PwmSession loopSession : currentValidSessionSet()) {
-                if (loopSession != null) {
-                    if (loopSession.getSessionManager().hasActiveLdapConnection()) {
-                        counter++;
-                    }
-                }
-            }
-        } catch (Exception e) {
-            LOGGER.error("unexpected error counting ldap connections: " + e.getMessage());
-        }
-        return counter;
-    }
-
     private Set<PwmSession> currentValidSessionSet() {
         final Set<PwmSession> returnSet = new HashSet<>();
         for (final PwmSession pwmSession : copyOfSessionSet()) {

+ 4 - 2
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -22,7 +22,7 @@
 
 package password.pwm.svc.telemetry;
 
-import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import lombok.Builder;
 import lombok.Getter;
 import password.pwm.AppProperty;
@@ -140,6 +140,8 @@ public class TelemetryService implements PwmService {
         executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, TelemetryService.class);
 
         scheduleNextJob();
+
+        status = STATUS.OPEN;
     }
 
     private void initSender() throws PwmUnrecoverableException
@@ -256,7 +258,7 @@ public class TelemetryService implements PwmService {
             }
         }
 
-        final Set<ChaiProvider.DIRECTORY_VENDOR> ldapVendors = new LinkedHashSet<>();
+        final Set<DirectoryVendor> ldapVendors = new LinkedHashSet<>();
         for (final LdapProfile ldapProfile : config.getLdapProfiles().values()) {
             try {
                 ldapVendors.add(ldapProfile.getProxyChaiProvider(pwmApplication).getDirectoryVendor());

+ 16 - 0
server/src/main/java/password/pwm/util/LocaleHelper.java

@@ -50,6 +50,7 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -600,4 +601,19 @@ public class LocaleHelper {
 
         return PwmConstants.DEFAULT_LOCALE;
     }
+
+    public static String getBrowserLocaleString(final Locale locale)
+    {
+        return locale == null
+                ? ""
+                : locale.toString().replace("_","-");
+    }
+
+    public static Comparator<Locale> localeComparator(final Locale comparisonLocale) {
+        return (o1, o2) -> {
+            final String name1 = o1.getDisplayName(comparisonLocale);
+            final String name2 = o2.getDisplayName(comparisonLocale);
+            return name1.compareToIgnoreCase(name2);
+        };
+    }
 }

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

@@ -283,7 +283,7 @@ public class PwmPasswordRuleValidator {
             final int requiredPasswordStrength = ruleHelper.readIntValue(PwmPasswordRule.MinimumStrength);
             if (requiredPasswordStrength > 0) {
                 if (pwmApplication != null) {
-                    final int passwordStrength = PasswordUtility.judgePasswordStrength(passwordString);
+                    final int passwordStrength = PasswordUtility.judgePasswordStrength(pwmApplication.getConfig(), passwordString);
                     if (passwordStrength < requiredPasswordStrength) {
                         errorList.add(new ErrorInformation(PwmError.PASSWORD_TOO_WEAK));
                         //LOGGER.trace(pwmSession, "password rejected, password strength of " + passwordStrength + " is lower than policy requirement of " + requiredPasswordStrength);

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

@@ -226,7 +226,7 @@ public class RandomPasswordGenerator {
                 LOGGER.trace(sessionLabel, "finished random password generation in " + td.asCompactString() + " after " + tryCount + " tries.");
             } else {
                 final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(password.toString(), null, null);
-                final int judgeLevel = PasswordUtility.judgePasswordStrength(password.toString());
+                final int judgeLevel = PasswordUtility.judgePasswordStrength(pwmApplication.getConfig(), password.toString());
                 final StringBuilder sb = new StringBuilder();
                 sb.append("failed random password generation after ").append(td.asCompactString()).append(" after ").append(tryCount).append(" tries. ");
                 sb.append("(errors=").append(errors.size()).append(", judgeLevel=").append(judgeLevel);

+ 1 - 1
server/src/main/java/password/pwm/util/cli/commands/ImportResponsesCommand.java

@@ -63,7 +63,7 @@ public class ImportResponsesCommand extends AbstractCliCommand {
 
                 final UserIdentity userIdentity = UserIdentity.fromDelimitedKey(inputData.username);
                 final ChaiUser user = pwmApplication.getProxiedChaiUser(userIdentity);
-                if (user.isValid()) {
+                if (user.exists()) {
                     out("writing responses to user '" + user.getEntryDN() + "'");
                     try {
                         final ChallengeProfile challengeProfile = pwmApplication.getCrService().readUserChallengeProfile(

+ 2 - 1
server/src/main/java/password/pwm/util/cli/commands/LdapSchemaExtendCommand.java

@@ -51,7 +51,8 @@ public class LdapSchemaExtendCommand extends AbstractCliCommand {
             console.writer().flush();
             bindPW = new String(console.readPassword());
         }
-        final ChaiProvider chaiProvider = ChaiProviderFactory.createProvider(ldapUrl, bindDN, bindPW);
+        final ChaiProviderFactory chaiProviderFactory = cliEnvironment.getPwmApplication().getLdapConnectionService().getChaiProviderFactory();
+        final ChaiProvider chaiProvider = chaiProviderFactory.newProvider(ldapUrl, bindDN, bindPW);
         final SchemaOperationResult operationResult = SchemaManager.extendSchema(chaiProvider);
         final boolean checkOk = operationResult.isSuccess();
         if (checkOk) {

+ 2 - 5
server/src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java

@@ -285,10 +285,7 @@ class DatabaseAccessorImpl implements DatabaseAccessor {
 
 
             try (PreparedStatement statement = connection.prepareStatement(sqlText)) {
-                try (ResultSet resultSet = statement.executeQuery()) {
-                    connection.commit();
-                    return resultSet;
-                }
+                return statement.executeQuery();
             } catch (SQLException e) {
                 processSqlException(null, e);
             }
@@ -331,7 +328,7 @@ class DatabaseAccessorImpl implements DatabaseAccessor {
                 try {
                     resultSet.close();
                 } catch (SQLException e) {
-                    LOGGER.error("error closing inner resultset in iterator: " + e.getMessage());
+                    LOGGER.error("error closing inner resultSet in iterator: " + e.getMessage());
                 }
             }
             finished = true;

+ 56 - 0
server/src/main/java/password/pwm/util/localdb/LocalDBService.java

@@ -0,0 +1,56 @@
+package password.pwm.util.localdb;
+
+import password.pwm.PwmApplication;
+import password.pwm.config.option.DataStorageMethod;
+import password.pwm.error.PwmException;
+import password.pwm.health.HealthRecord;
+import password.pwm.svc.PwmService;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LocalDBService implements PwmService {
+    private PwmApplication pwmApplication;
+
+    @Override
+    public STATUS status() {
+        if ( pwmApplication != null
+                && pwmApplication.getLocalDB() != null
+                && pwmApplication.getLocalDB().status() == LocalDB.Status.OPEN)
+        {
+            return STATUS.OPEN;
+        }
+
+        return STATUS.CLOSED;
+    }
+
+    @Override
+    public void init(final PwmApplication pwmApplication) throws PwmException {
+        this.pwmApplication = pwmApplication;
+    }
+
+    @Override
+    public void close() {
+        //no-op
+    }
+
+    @Override
+    public List<HealthRecord> healthCheck() {
+        return null;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo() {
+        final Map<String,String> returnInfo = new LinkedHashMap<>();
+        if (status() == STATUS.OPEN) {
+            final Map<String,Serializable> localDbInfo = pwmApplication.getLocalDB().debugInfo();
+            for (final Map.Entry<String,Serializable> entry : localDbInfo.entrySet()) {
+                returnInfo.put( entry.getKey(), String.valueOf(entry.getValue()) );
+            }
+        }
+        return new ServiceInfoBean(Collections.singleton(DataStorageMethod.LOCALDB), Collections.unmodifiableMap(returnInfo));
+    }
+}

+ 1 - 0
server/src/main/java/password/pwm/util/localdb/Xodus_LocalDB.java

@@ -159,6 +159,7 @@ public class Xodus_LocalDB implements LocalDBProvider {
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
         environmentConfig.setEnvCloseForcedly(true);
         environmentConfig.setMemoryUsage(50 * 1024 * 1024);
+        environmentConfig.setEnvGatherStatistics(true);
 
         for (final Map.Entry<String,String> entry : initParameters.entrySet()) {
             final String key = entry.getKey();

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

@@ -39,7 +39,7 @@ import java.time.Instant;
 @EqualsAndHashCode
 public class PwmLogEvent implements Serializable, Comparable {
 
-    private static final int MAX_MESSAGE_LENGTH = 100_000;
+    private static final int MAX_MESSAGE_LENGTH = 50_000;
 
     private final PwmLogLevel level;
 

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

@@ -32,7 +32,7 @@ import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ChaiValidationException;
 import com.novell.ldapchai.impl.edir.NmasCrFactory;
-import com.novell.ldapchai.provider.ChaiProvider;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.bean.ResponseInfoBean;
@@ -132,7 +132,7 @@ public class CrService implements PwmService {
 
         if (config.readSettingAsBoolean(PwmSetting.EDIRECTORY_READ_CHALLENGE_SET)) {
             try {
-                if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+                if (theUser.getChaiProvider().getDirectoryVendor() == DirectoryVendor.EDIRECTORY) {
                     if (policy != null && policy.getChaiPasswordPolicy() != null) {
                         returnSet = NmasCrFactory.readAssignedChallengeSet(theUser.getChaiProvider(), policy.getChaiPasswordPolicy(), locale);
                     }

+ 56 - 15
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -22,7 +22,6 @@
 
 package password.pwm.util.operations;
 
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiPasswordPolicy;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiException;
@@ -32,9 +31,11 @@ import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
 import com.novell.ldapchai.provider.ChaiConfiguration;
 import com.novell.ldapchai.provider.ChaiProvider;
-import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import com.novell.ldapchai.util.ChaiUtility;
+import com.nulabinc.zxcvbn.Strength;
+import com.nulabinc.zxcvbn.Zxcvbn;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
@@ -44,6 +45,7 @@ import password.pwm.bean.PasswordStatus;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.option.StrengthMeterType;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -91,7 +93,6 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -271,7 +272,7 @@ public class PasswordUtility {
 
         boolean setPasswordWithoutOld = false;
         if (oldPassword == null) {
-            if (pwmSession.getSessionManager().getActor(pwmApplication).getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
+            if (pwmSession.getSessionManager().getActor(pwmApplication).getChaiProvider().getDirectoryVendor() == DirectoryVendor.ACTIVE_DIRECTORY) {
                 setPasswordWithoutOld = true;
             }
         }
@@ -359,7 +360,7 @@ public class PasswordUtility {
         final String bindDN;
 
         try {
-            final ChaiUser theUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider);
+            final ChaiUser theUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
             bindDN = chaiProvider.getChaiConfiguration().getSetting(ChaiSetting.BIND_DN);
             bindIsSelf = userIdentity.canonicalEquals(new UserIdentity(bindDN, userIdentity.getLdapProfileID()), pwmApplication);
 
@@ -397,7 +398,7 @@ public class PasswordUtility {
         // update stats
         pwmApplication.getStatisticsManager().updateEps(EpsStatistic.PASSWORD_CHANGES,1);
 
-        final int passwordStrength = PasswordUtility.judgePasswordStrength(newPassword.getStringValue());
+        final int passwordStrength = PasswordUtility.judgePasswordStrength(pwmApplication.getConfig(), newPassword.getStringValue());
         pwmApplication.getStatisticsManager().updateAverageValue(Statistic.AVG_PASSWORD_STRENGTH,passwordStrength);
 
         // at this point the password has been changed, so log it.
@@ -563,7 +564,7 @@ public class PasswordUtility {
             final String loopReplicaUrl = loopConfiguration.getSetting(ChaiSetting.BIND_DN);
             ChaiProvider loopProvider = null;
             try {
-                loopProvider = ChaiProviderFactory.createProvider(loopConfiguration);
+                loopProvider = pwmApplication.getLdapConnectionService().getChaiProviderFactory().newProvider(loopConfiguration);
                 final Instant lastModifiedDate = determinePwdLastModified(pwmApplication, sessionLabel, userIdentity);
                 returnValue.put(loopReplicaUrl, lastModifiedDate);
             } catch (ChaiUnavailableException e) {
@@ -671,6 +672,46 @@ public class PasswordUtility {
     */
 
     public static int judgePasswordStrength(
+            final Configuration configuration,
+            final String password
+    )
+            throws PwmUnrecoverableException
+    {
+        final StrengthMeterType strengthMeterType = configuration.readSettingAsEnum(PwmSetting.PASSWORD_STRENGTH_METER_TYPE, StrengthMeterType.class);
+        switch (strengthMeterType) {
+            case ZXCVBN:
+                return judgePasswordStrengthUsingZxcvbnAlgorithm(configuration, password);
+
+            case PWM:
+                return judgePasswordStrengthUsingTraditionalAlgorithm(password);
+
+            default:
+                JavaHelper.unhandledSwitchStatement(strengthMeterType);
+        }
+
+        return -1;
+    }
+
+    public static int judgePasswordStrengthUsingZxcvbnAlgorithm(
+            final Configuration configuration,
+            final String password
+    ) {
+        final Zxcvbn zxcvbn = new Zxcvbn();
+        final Strength strength = zxcvbn.measure(password);
+
+        final int zxcvbnScore = strength.getScore();
+
+        // zxcvbn returns a score of 0-4 (see: https://github.com/dropbox/zxcvbn)
+        switch (zxcvbnScore) {
+            case 4: return Integer.parseInt(configuration.readAppProperty(AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_STRONG));
+            case 3: return Integer.parseInt(configuration.readAppProperty(AppProperty.PASSWORD_STRENGTH_THRESHOLD_STRONG));
+            case 2: return Integer.parseInt(configuration.readAppProperty(AppProperty.PASSWORD_STRENGTH_THRESHOLD_GOOD));
+            case 1: return Integer.parseInt(configuration.readAppProperty(AppProperty.PASSWORD_STRENGTH_THRESHOLD_WEAK));
+            default: return Integer.parseInt(configuration.readAppProperty(AppProperty.PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK));
+        }
+    }
+
+    public static int judgePasswordStrengthUsingTraditionalAlgorithm (
             final String password
     )
             throws PwmUnrecoverableException
@@ -914,7 +955,7 @@ public class PasswordUtility {
             }
         }
 
-        final int strength = judgePasswordStrength(password == null ? null : password.getStringValue());
+        final int strength = judgePasswordStrength(pwmApplication.getConfig(), password == null ? null : password.getStringValue());
         return new PasswordCheckInfo(userMessage, pass, strength, matchStatus, errorCode);
     }
 
@@ -1030,10 +1071,10 @@ public class PasswordUtility {
     {
         // fetch last password modification time from pwm last update attribute operation
         try {
-            final Date chaiReadDate = theUser.readPasswordModificationDate();
+            final Instant chaiReadDate = theUser.readPasswordModificationDate();
             if (chaiReadDate != null) {
                 LOGGER.trace(sessionLabel, "read last user password change timestamp (via chai) as: " + JavaHelper.toIsoDate(chaiReadDate));
-                return chaiReadDate.toInstant();
+                return chaiReadDate;
             }
         } catch (ChaiOperationException e) {
             LOGGER.error(sessionLabel, "unexpected error reading password last modified timestamp: " + e.getMessage());
@@ -1043,9 +1084,9 @@ public class PasswordUtility {
         final String pwmLastSetAttr = ldapProfile.readSettingAsString(PwmSetting.PASSWORD_LAST_UPDATE_ATTRIBUTE);
         if (pwmLastSetAttr != null && pwmLastSetAttr.length() > 0) {
             try {
-                final Date pwmPwdLastModified = theUser.readDateAttribute(pwmLastSetAttr);
+                final Instant pwmPwdLastModified = theUser.readDateAttribute(pwmLastSetAttr);
                 LOGGER.trace(sessionLabel, "read pwmPasswordChangeTime as: " + (pwmPwdLastModified == null ? "n/a" : JavaHelper.toIsoDate(pwmPwdLastModified)));
-                return pwmPwdLastModified == null ? null : pwmPwdLastModified.toInstant();
+                return pwmPwdLastModified;
             } catch (ChaiOperationException e) {
                 LOGGER.error(sessionLabel, "error parsing password last modified PWM password value for user " + theUser.getEntryDN() + "; error: " + e.getMessage());
             }
@@ -1067,11 +1108,11 @@ public class PasswordUtility {
 
         // for oracle DS; this check is also handled in UserAuthenticator.
         try {
-            if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS == chaiUser.getChaiProvider().getDirectoryVendor()) {
+            if (DirectoryVendor.ORACLE_DS == chaiUser.getChaiProvider().getDirectoryVendor()) {
                 final String oracleDS_PrePasswordAllowChangeTime = chaiUser.readStringAttribute("passwordAllowChangeTime");
                 if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) {
-                    final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
-                    if (new Date().before(date)) {
+                    final Instant date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
+                    if (Instant.now().isBefore(date)) {
                         LOGGER.debug("discovered oracleds allowed change time is set to: " + JavaHelper.toIsoDate(date) + ", won't permit password change");
                         final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(date);
                         final ErrorInformation errorInformation = new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg);

+ 13 - 9
server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java

@@ -24,7 +24,6 @@ package password.pwm.util.operations.cr;
 
 import com.novell.ldap.LDAPConnection;
 import com.novell.ldap.LDAPException;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.cr.ChaiChallenge;
 import com.novell.ldapchai.cr.ChaiChallengeSet;
@@ -44,6 +43,7 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiProviderFactory;
 import com.novell.ldapchai.provider.ChaiProviderImplementor;
 import com.novell.ldapchai.provider.ChaiSetting;
+import com.novell.ldapchai.provider.DirectoryVendor;
 import com.novell.ldapchai.provider.JLDAPProviderImpl;
 import com.novell.security.nmas.client.NMASCallback;
 import com.novell.security.nmas.client.NMASCompletionCallback;
@@ -244,7 +244,7 @@ public class NMASCrOperator implements CrOperator {
         pwmApplication.getIntruderManager().convenience().checkUserIdentity(userIdentity);
 
         try {
-            if (theUser.getChaiProvider().getDirectoryVendor() != ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+            if (theUser.getChaiProvider().getDirectoryVendor() != DirectoryVendor.EDIRECTORY) {
                 return null;
             }
 
@@ -266,7 +266,7 @@ public class NMASCrOperator implements CrOperator {
             throws PwmUnrecoverableException
     {
         try {
-            if (theUser.getChaiProvider().getDirectoryVendor() != ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+            if (theUser.getChaiProvider().getDirectoryVendor() != DirectoryVendor.EDIRECTORY) {
                 LOGGER.debug("skipping request to read NMAS responses for " + userIdentity + ", directory type is not eDirectory");
                 return null;
             }
@@ -290,7 +290,7 @@ public class NMASCrOperator implements CrOperator {
             throws PwmUnrecoverableException
     {
         try {
-            if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+            if (theUser.getChaiProvider().getDirectoryVendor() == DirectoryVendor.EDIRECTORY) {
                 NmasCrFactory.clearResponseSet(theUser);
                 LOGGER.info("cleared responses for user " + theUser.getEntryDN() + " using NMAS method ");
             }
@@ -311,7 +311,7 @@ public class NMASCrOperator implements CrOperator {
             throws PwmUnrecoverableException
     {
         try {
-            if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) {
+            if (theUser.getChaiProvider().getDirectoryVendor() == DirectoryVendor.EDIRECTORY) {
 
                 final NmasResponseSet nmasResponseSet = NmasCrFactory.newNmasResponseSet(
                         responseInfoBean.getCrMap(),
@@ -389,9 +389,12 @@ public class NMASCrOperator implements CrOperator {
             final List<String> ldapURLs = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_SERVER_URLS);
             final String proxyDN = ldapProfile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN);
             final PasswordData proxyPW = ldapProfile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD);
-            chaiConfiguration = LdapOperationsHelper.createChaiConfiguration(config, ldapProfile, ldapURLs, proxyDN,
+            final ChaiConfiguration newChaiConfig = LdapOperationsHelper.createChaiConfiguration(config, ldapProfile, ldapURLs, proxyDN,
                     proxyPW);
-            chaiConfiguration.setSetting(ChaiSetting.PROVIDER_IMPLEMENTATION, JLDAPProviderImpl.class.getName());
+
+            chaiConfiguration = ChaiConfiguration.builder(newChaiConfig)
+                    .setSetting(ChaiSetting.PROVIDER_IMPLEMENTATION, JLDAPProviderImpl.class.getName())
+                    .build();
 
             cycle();
         }
@@ -408,8 +411,9 @@ public class NMASCrOperator implements CrOperator {
         }
 
         private LDAPConnection makeLdapConnection() throws Exception {
-            final ChaiProvider chaiProvider = ChaiProviderFactory.createProvider(chaiConfiguration);
-            final ChaiUser theUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider);
+            final ChaiProviderFactory chaiProviderFactory = pwmApplication.getLdapConnectionService().getChaiProviderFactory();
+            final ChaiProvider chaiProvider = chaiProviderFactory.newProvider(chaiConfiguration);
+            final ChaiUser theUser = chaiProvider.getEntryFactory().newChaiUser(userIdentity.getUserDN());
             try {
                 if (theUser.isPasswordLocked()) {
                     LOGGER.trace("user " + theUser.getEntryDN() + " appears to be intruder locked, aborting nmas ResponseSet loading" );

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

@@ -1,7 +1,6 @@
 package password.pwm.ws.server;
 
 import com.google.gson.stream.MalformedJsonException;
-import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -306,7 +305,7 @@ public abstract class RestServlet extends HttpServlet{
 
         public ChaiUser getChaiUser() throws PwmUnrecoverableException {
             try {
-                return ChaiFactory.createChaiUser(userIdentity.getUserDN(), getChaiProvider());
+                return getChaiProvider().getEntryFactory().newChaiUser(userIdentity.getUserDN());
             } catch (ChaiUnavailableException e) {
                 throw PwmUnrecoverableException.fromChaiException(e);
             }

+ 5 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -225,6 +225,11 @@ otp.encryptionAlg=AES
 password.randomGenerator.maxAttempts=2000
 password.randomGenerator.maxLength=1024
 password.randomGenerator.jitter.count=50
+password.strength.threshold.veryStrong=100
+password.strength.threshold.strong=75
+password.strength.threshold.good=45
+password.strength.threshold.weak=20
+password.strength.threshold.veryWeak=0
 peoplesearch.values.verifyUserDN=true
 peoplesearch.values.maxCount=100
 peoplesearch.view.detail.links=

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

@@ -143,6 +143,7 @@
             <value><![CDATA[ja::jp]]></value>
             <value><![CDATA[ko::kr]]></value>
             <value><![CDATA[nl::nl]]></value>
+            <value><![CDATA[nb::no]]></value>
             <value><![CDATA[no::no]]></value>
             <value><![CDATA[nn::no]]></value>
             <value><![CDATA[pl::pl]]></value>
@@ -191,6 +192,15 @@
             <value>true</value>
         </default>
     </setting>
+    <setting hidden="false" key="password.strengthMeter.type" level="1" required="true">
+        <options>
+            <option value="PWM">Traditional - built in algorithm</option>
+            <option value="ZXCVBN">zxcvbn - open source library</option>
+        </options>
+        <default>
+            <value>PWM</value>
+        </default>
+    </setting>
     <setting hidden="false" key="display.showHidePasswordFields" level="1" required="true">
         <default>
             <value>true</value>

+ 3 - 1
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -124,9 +124,11 @@ Display_PasswordExpired=Your password has expired.  You must set a new password
 Display_PasswordGeneration=The following passwords have been randomly generated for you.  These passwords are based on real words to make them easier to remember, but have been modified to make them difficult to guess.
 Display_PasswordNoExpire=Your password does not expire.
 Display_PasswordPrompt=Please type your new password
+Display_PasswordStrengthVeryHigh=Strength\: <b>Very Strong</b>
 Display_PasswordStrengthHigh=Strength\: <b>Strong</b>
-Display_PasswordStrengthLow=Strength\: <b>Weak</b>
 Display_PasswordStrengthMedium=Strength\: <b>Good</b>
+Display_PasswordStrengthLow=Strength\: <b>Weak</b>
+Display_PasswordStrengthVeryLow=Strength\: <b>Very Weak</b>
 Display_PasswordReplicationStatus=Password replication (progress %1%)
 Display_PasswordWarn=<b>Your password will soon expire</b>.  Please change your password soon to avoid problems accessing this service.  <br/><br/>Your password will expire on %1%.
 Display_PeopleSearch=Please type your search query below.  You may search for a person based on name, email address or telephone number.

+ 339 - 0
server/src/main/resources/password/pwm/i18n/Display_nb.properties

@@ -0,0 +1,339 @@
+#Thu, 10 Aug 2017 01:24:26 -0400
+#=
+# Password Management Servlets (PWM)
+# http://www.pwm-project.org
+#=
+# Copyright (c) 2006-2009 Novell, Inc.
+# Copyright (c) 2009-2017 The PWM Project
+#=
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#=
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#=
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#=
+
+Button_Activate=Aktiver
+Button_Attributes=Brukerdata
+Button_Agree=Aksepter
+Button_Browse=Bla
+Button_Cancel=Avbryt
+Button_ChangePassword=Endre passord
+Button_ChangeResponses=Tilbake
+Button_CheckCode=Sjekk kode
+Button_ClearOtpReEnroll=Registrer mobilenhet p\u00e5 nytt
+Button_HelpdeskClearOtpSecret=Fjern OTP kode
+Button_ClearResponses=Fjern svar
+Button_CloseWindow=Lukk vindu
+Button_Confirm=Bekreft
+Button_ConfirmResponses=Bekreft sikkerhetssvar
+Button_Continue=Fortsett
+Button_Create=Opprett
+Button_Delete=Slett
+Button_Email=Epost
+Button_GoBack=Tilbake
+Button_Hide=Skjul
+Button_Hide_Responses=Skjul svar
+Button_Home=Hjem
+Button_Login=Logg inn
+Button_Logout=Logg ut
+Button_More=Mer
+Button_OrgChart=Organisasjonskart
+Button_OTP=OTP
+Button_RecoverPassword=Sjekk svar
+Button_Reset=Fjern
+Button_Search=S\u00f8k
+Button_SetResponses=Lagre svar
+Button_Show=Vis
+Button_Show_Responses=Vis svar
+Button_Skip=Hopp over
+Button_SMS=SMS
+Button_TokenResend=Send kode p\u00e5 nytt
+Button_Unlock=L\u00e5s opp
+Button_UnlockPassword=L\u00e5s opp passord
+Button_Update=Oppdater
+Button_Verify=Verifiser
+Button_Verificiations=Verifiseringer
+Button_OK=OK
+Display_ActivateUser=For \u00e5 bekrefte identiteten din, fyll ut f\u00f8lgende informasjon. Din informasjon vil bli brukt til \u00e5 finne og aktivere din brukerkonto.<p/>S\u00f8rg for \u00e5 fullf\u00f8re prosessen. Hvis du avbryter, vil ikke brukerkontoen din bli korrekt aktivert.
+Display_AutoGeneratedPassword=Auto-generere et nytt passord
+Display_CapsLockIsOn=CAPS LOCK er p\u00e5
+Display_Captcha=Vennligst skriv hele bekreftelseskoden nedenfor. Hensikten med denne koden er \u00e5 beskytte brukerkontoenkontoen din mot misbruk.
+Display_CaptchaInputWords=Skriv inn teksten som vises ovenfor
+Display_CaptchaInputNumbers=Skriv inn tallene du h\u00f8rer
+Display_CaptchaGetAudio=F\u00e5 lyd av CAPTCHA
+Display_CaptchaGetImage=F\u00e5 bilde av CAPTCHA
+Display_CaptchaHelp=Hjelp
+Display_CaptchaRefresh=Ny
+Display_ChangePassword=Vennligst endre passordet ditt. Pass godt p\u00e5 passordet ditt. N\u00e5r du har skrevet inn nytt passord, klikker du p\u00e5 knappen Endre passord. Hvis du m\u00e5 notere ditt nye passord, s\u00f8rg for \u00e5 oppbevare det p\u00e5 en trygg m\u00e5te. Ditt nye passord m\u00e5 oppfylle f\u00f8lgende krav\:
+Display_ChangePasswordForm=Vennligst skriv inn f\u00f8lgende data. Dette kreves for \u00e5 bekrefte identiteten din f\u00f8r du kan endre passordet ditt.
+Display_CheckingData=Kontrollerer informasjon....
+Display_CheckingPassword=Kontrollerer passord....
+Display_CheckingResponses=Kontrollerer svar....
+Display_ClientDisconnect=Kan ikke kommunisere med server.
+Display_CommunicationError=Kan ikke kommunisere med server. Fortsett n\u00e5r klar.
+Display_ConfirmResponses=Pass p\u00e5 at svarene dine og sp\u00f8rsm\u00e5lene er riktige. Kontroller staving og tegnsetting. I tilfelle du glemmer passordet, m\u00e5 du skrive inn de eksakte svarene p\u00e5 sp\u00f8rsm\u00e5lene nedenfor.
+Display_Day=dag
+Display_Days=dager
+Display_DeleteUserConfirm=Er du sikker p\u00e5 at du vil slette din konto? Dette kan ikke angres.
+Display_ErrorBody=En feil har oppst\u00e5tt. Vennligst lukk nettleseren din, og pr\u00f8v igjen senere. Hvis denne feilen oppst\u00e5r gjentatte ganger, m\u00e5 du kontakte brukerst\u00f8tte.
+Display_ErrorReference=Feilreferanse %1%
+Display_ExpirationDate=Brukerkontoens utl\u00f8psdato (maks %1% dager)
+Display_FooterInfoText=
+Display_ForgottenPassword=Hvis du har glemt passordet ditt, f\u00f8lg instruksjonene for \u00e5 f\u00e5 mulighet til \u00e5 sette nytt passord.
+Display_ForgottenUsername=Vennligst skriv inn f\u00f8lgende informasjon. Denne informasjonen vil bli brukt til \u00e5 finne ditt glemte brukernavn.
+Display_GuestRegistration=For \u00e5 opprette en ny gjestekonto, vennligst skriv f\u00f8lgende informasjon.
+Display_GuestUpdate=For \u00e5 oppdatere en gjestekonto, kan du sjekke og eventuelt endre f\u00f8lgende informasjon.
+Display_Helpdesk=Vennligst skriv inn dine s\u00f8kedata for brukeren.
+Display_HelpdeskOtpValidation=Be brukeren om \u00e5 laste inn sin mobile autentiseringsapp og dele gjeldende passord-kode.
+Display_HelpdeskNoData=Serveren svarer ikke som den skal. En \u00f8kt i et annet webleservindu kan v\u00e6re utl\u00f8pt. Last inn siden p\u00e5 nytt, og logg p\u00e5 igjen.
+Display_Hour=time
+Display_Hours=timer
+Display_IdleTimeout=Tidsavbrudd for ingen aktivitet\:
+Display_IdleWarningMessage=Nettlesersesjonen din er i ferd med \u00e5 utl\u00f8pte. Klikk hvor som helst p\u00e5 denne siden for \u00e5 fortsette.
+Display_IdleWarningTitle=Tidsavbrudd for ingen aktivitet
+Display_JavascriptRequired=Javascript er p\u00e5krevet for denne siden.
+Display_LeaveDirtyPasswordPage=Hvis du forlater denne siden, vil passordet ditt ikke endres.
+Display_Login=
+Display_LoginPasswordOnly=Vennligst skriv inn passordet ditt nedenfor. Ditt n\u00e5v\u00e6rende passord er n\u00f8dvendig for \u00e5 f\u00e5 tilgang til dette programmet.
+Display_Logout=Du er n\u00e5 logget ut.
+Display_LogoutPublic=Din nettlesersesjon har g\u00e5tt ut p\u00e5 tid.
+Display_Minute=minutter
+Display_Minutes=minutter
+Display_NAAF_PASSWORD=Skriv inn ditt NAAF autentiserings-passord.
+Display_NAAF_LDAP_PASSWORD=Skriv inn ditt LDAP autentiserings-passord.
+Display_NAAF_SECURITY_QUESTIONS=Oppgi dine sikkerhetssp\u00f8rsm\u00e5l.
+Display_NAAF_EMAIL_OTP=En epost har blitt sendt med ditt engangspassord.
+Display_NAAF_SMS_OTP=En SMS har blitt sendt med ditt engangspassord.
+Display_NAAF_SMARTPHONE=Verifikasjonsprosessen har startet p\u00e5 smarttelefonen. Fortsett n\u00e5r ferdig.
+Display_NAAF_RADIUS=Skriv inn ditt RADIUS autentiserings-passord.
+Display_NAAF_TOTP=Skriv inn din TOTP verdi.
+Display_NAAF_HOTP=Skriv inn din HOTP verdi.
+Display_NAAF_VOICE=Verifikasjonsprosessen for stemme har startet. Fortsett n\u00e5r ferdig.
+Display_NewUser=For \u00e5 opprette en ny brukerkonto m\u00e5 du fylle ut f\u00f8lgende skjema.
+Display_NewUserProfile=For \u00e5 registrere en ny konto, velg en profil.
+Display_PasswordExpired=Passordet ditt er utl\u00f8ptt. Du m\u00e5 sette et nytt passord n\u00e5.
+Display_PasswordGeneration=F\u00f8lgende passord har blitt tilfeldig generert for deg. Disse passordene er basert p\u00e5 virkelige ord for \u00e5 gj\u00f8re dem lettere \u00e5 huske, men har blitt modifisert for \u00e5 gj\u00f8re dem vanskelige \u00e5 gjette.
+Display_PasswordNoExpire=Passordet ditt har ikke tidsbegrenset utl\u00f8psdato.
+Display_PasswordPrompt=Vennligst skriv inn nytt passord
+Display_PasswordStrengthVeryHigh=Strength: <b>Veldig sterkt </b>
+Display_PasswordStrengthHigh=Strength: <b>Sterkt </b>
+Display_PasswordStrengthMedium=Strength: <b>Bra </b>
+Display_PasswordStrengthLow=Strength: <b>Svakt </b>
+Display_PasswordStrengthVeryLow=Strength: <b>Veldig svakt </b>
+Display_PasswordReplicationStatus=Passord-replikering (fremdrift %1%)
+Display_PasswordWarn=<b>Ditt passord vil snart utl\u00f8pe</b>.  Vennligst endre passordet ditt snarest for \u00e5 unng\u00e5 problemer med tilgang til tjenester.  <br/><br/>Passordet vil utl\u00f8pe %1%.
+Display_PeopleSearch=Vennligst skriv inn s\u00f8kedataene nedenfor. Du kan s\u00f8ke etter en person ved \u00e5 oppgi navn, e-postadresse eller telefonnummer.
+Display_PleaseWait=Laster...
+Display_PleaseWaitNewUser=Din nye brukerkonto blir konfigurert. Denne prosessen kan ta litt tid, v\u00e6r t\u00e5lmodig.
+Display_PleaseWaitPassword=Passordet ditt blir satt. Denne prosessen kan ta litt tid, s\u00e5 v\u00e6r t\u00e5lmodig.
+Display_Random=Tilfeldig
+Display_RecoverVerificationChoice=Vennligst velg en av f\u00f8lgende metoder for \u00e5 bekrefte identiteten din. Merk\: Hvis administratoren krever flere former for bekreftelse, blir du omdirigert tilbake til denne siden til alle bekreftelseskriteriene er oppfylt.
+Display_RecoverTokenSendChoices=For \u00e5 bekrefte identiteten din, sendes en sikkerhetskode til deg. Vennligst velg hvilken metode du foretrekker for \u00e5 motta sikkerhetskoden din.
+Display_RecoverTokenSendChoiceEmail=Send kode til registrert epostadresse.
+Display_RecoverTokenSendChoiceSMS=Send kode til mobiltelefon med SMS.
+Display_RecoverChoiceReset=Sett et nytt passord. Hvis du har glemt passordet ditt og \u00f8nsker \u00e5 sette et nytt, klikk her. Kontoen din vil ogs\u00e5 l\u00e5ses opp n\u00e5r du setter et nytt passord.
+Display_RecoverChoiceUnlock=L\u00e5s opp kontoen din igjen. Hvis du husker passordet ditt, kan du l\u00e5se opp kontoen din igjen ved \u00e5 velge dette alternativet. Passordet ditt vil ikke bli endret.
+Display_RecoverEnterCode=For \u00e5 bekrefte identiteten har en engangskode blitt sendt til %1%. Velg koblingen i eposten eller kopier inn den mottatte sikkerhetskoden her.
+Display_RecoverEnterCodeSMS=For \u00e5 bekrefte identiteten har en engangskode blitt sendt til din telefon p\u00e5 %1%. Oppgi den mottatte sikkerhetskoden her.
+Display_RecoverPassword=Vennligst svar p\u00e5 f\u00f8lgende sp\u00f8rsm\u00e5l. Hvis du svarer riktig p\u00e5 alle disse sp\u00f8rsm\u00e5lene, vil du f\u00e5 lov til \u00e5 sette nytt passord.
+Display_RecoverPasswordChoices=Din konto er blitt sperret p\u00e5 grunn av for mange innloggingsfors\u00f8k med galt passord. Du kan fortsette ved \u00e5 l\u00e5se opp kontoen din igjen, eller ved \u00e5 endre passordet ditt.
+Display_RecoverRandomResponses=Du m\u00e5 svare p\u00e5 f\u00f8lgende sp\u00f8rsm\u00e5l for \u00e5 fortsette.
+Display_RecoverRequiredResponses=Det er et krav at du svarer p\u00e5 disse sp\u00f8rm\u00e5lene.
+Display_RecoverOTP=For \u00e5 bekrefte identiteten din, bruk din mobilenhet for \u00e5 generere sikkerhetskoden.
+Display_RecoverOTPIdentified=For \u00e5 bekrefte identiteten din, bruk din mobilenhet til \u00e5 generere en sikkerhetskod. Registreings ID-en for mobilenhet er <b>%1%</b>.
+Display_ResponsesPrompt=Vennligst skriv dine svar
+Display_SelectionIndicator=Velg et sp\u00f8rsm\u00e5l fra listen
+Display_SearchCompleted=S\u00f8k utf\u00f8rt.
+Display_SearchResultsInfo=Returnerte %1% resultater p\u00e5 %2%.
+Display_SearchResultsExceeded=S\u00f8keresultatene overskrider maksimal s\u00f8kest\u00f8rrelse
+Display_SetRandomPasswordPrompt=Sette et nytt tilfeldig passord for denne brukeren?
+Display_SearchResultsNone=Ingen resultater
+Display_Second=sekund
+Display_Seconds=sekunder
+Display_SetupHelpdeskResponses=<p>Administratoren krever at du oppgir f\u00f8lgende svar. Disse svarene brukes til \u00e5 bekrefte identiteten din i tilfelle du kontakter helpdesk for \u00e5 f\u00e5 hjelp.</p>
+Display_SetupRandomResponses=
+Display_SetupRequiredResponses=
+Display_SetupResponses=<p>I tilfelle du glemmer passordet, kan du f\u00e5 muligheten til \u00e5 lage nytt ved \u00e5 svare p\u00e5 sp\u00f8rsm\u00e5l som bare du vet svaret p\u00e5.</p><p>Velg sp\u00f8rsm\u00e5l og svar som kan brukes til \u00e5 bekrefte din identitet i tilfelle du glemmer passordet ditt. Fordi svarene p\u00e5 disse sp\u00f8rsm\u00e5lene kan brukes til \u00e5 f\u00e5 tilgang til kontoen din, m\u00e5 du huske \u00e5 oppgi svar som ikke er lett for andre \u00e5 gjette eller vite.</p>
+Display_SetupOtpSecret=Hvis du glemmer passordet ditt, kan du f\u00e5 tilgang til kontoen din ved hjelp av mobilenheten. F\u00f8lg instruksjonene nedenfor basert p\u00e5 enhetstypen din.
+Display_SetupOtp_Android_Title=Android
+Display_SetupOtp_Android_Steps=<b>Installer Google Authenticator-appen for Android.</b><ol><li>G\u00e5 til Google Play Store p\u00e5 telefonen din.</li><li> S\u00f8k etter <b> Google Authenticator</b>.<br/>(<a target\="playstore" href\="https\://play.google.com/store/apps/details?id\=com.google.android.apps.authenticator2">Last ned Fra Google Play Store</a>)</ li><li>Last ned og installer programmet.</ li></ ol><b>Deretter, \u00e5pner og konfigurerer du Google Authenticator.</b><ol><li>I Google Authenticator, trykk p\u00e5 Meny og velg "Konfigurer konto."</li><li>Velg "Skann en strekkode."</li><li> Bruk telefonens kamera for \u00e5 skanne strekkoden.</li><li>N\u00e5r du har skannet strekkoden, klikker du Fortsett-knappen.</li></ol>
+Display_SetupOtp_iPhone_Title=iPhone
+Display_SetupOtp_iPhone_Steps=<b>Velg App Store p\u00e5 din iPhone.</b><ol><li>P\u00e5 telefonenen, g\u00e5 til App Store.</li><li>S\u00f8k etter <b>Google Authenticator</b>.<br/>(<a target\="itunesstore" href\="https\://itunes.apple.com/us/app/google-authenticator/id388497605?mt\=8">Last ned fra App Store</a>)</li><li>Velg appen, og deretter velg Gratis for \u00e5 laste ned og installere.</li></ol><b>Deretter, \u00e5pner og konfigurerer du Google Authenticator.</b><ol><li>I Google Authenticator, vel "+" og "Skann en strekkode."</li><li> Bruk telefonens kamera for \u00e5 skanne strekkoden.</li><li>N\u00e5r du har skannet strekkoden, klikker du Fortsett-knappen.</li></ol>
+Display_SetupOtp_Other_Title=Andre
+Display_SetupOtp_Other_Steps=<b>Finn en kompatibel to-faktor app.</b><ul><li>Pr\u00f8v \u00e5 s\u00f8ke i enhetens appbutikk etter <b>Google Authenticator</b>. <br/>Mange enheter har kompatible apper. </li><li>Pr\u00f8v \u00e5 s\u00f8ke etter en app som st\u00f8tter "<b>TOTP sikkerhets-tokens"</b> eller "RFC6238"</li><li>Last ned og installer programmet.</li></ul><b> Deretter, \u00e5pne og konfigurere appen.</b><ol><li>Skriv inn dataene nedenfor eller skann koden som appen oppgir.</li><li>N\u00e5r du har konfigurert appen, klikker du fortsett knapp.</li></ol>
+Display_SetupOtp_VerificationCodeFormat=Bekreftelseskoden m\u00e5 inneholde seks numeriske sifre.
+Display_TokenDestination=Token destinasjon
+Display_TokenResend=Sikkerhetskoden din skal komme med en gang. Hvis du har ventet en stund og enn\u00e5 ikke har mottatt en kode, klikker du p\u00e5 knappen for \u00e5 sende ny kode for \u00e5 motta en ny kode.
+Display_UsernameHeader=@User\:ID@
+Display_UsernameFooter=@User\:ID@
+Display_WarnExistingOtpSecretTime=Du har allerede registrert enheten den <span class\="timestamp">%1%</span>.  Du kan teste ved \u00e5 skrive inn den genererte koden.  Dersom du fortsetter s\u00e5 kan du re-konfigurere enheten.
+Display_WarnExistingOtpSecret=Du har allerede registrert enheten.  Du kan teste ved \u00e5 skrive inn den genererte koden.  Dersom du fortsetter s\u00e5 kan du re-konfigurere enheten.
+Display_WarnExistingResponseTime=Du har allerede sp\u00f8rsm\u00e5l/svar den <span class\="timestamp">%1%</span>. Dersom du fortsetter s\u00e5 kan du legg inn nye svar p\u00e5 sp\u00f8rsm\u00e5lene.
+Display_WarnExistingResponse=Du har allerede oppgitt sp\u00f8rsm\u00e5l/svar. Dersom du fortsetter s\u00e5 kan du legg inn nye svar p\u00e5 sp\u00f8rsm\u00e5lene.
+Display_PleaseVerifyOtp=Vennligst skriv inn den sekssifrede bekreftelseskoden fra enheten. Hvis enheten din ikke er konfigurert til \u00e5 gi deg en bekreftelseskode, kan du g\u00e5 tilbake til forrige side og konfigurere enheten.
+Display_OtpRecoveryInfo=Hver av disse gjenopprettingskodene kan brukes n\u00f8yaktig en gang i tilfelle du ikke f\u00e5r tilgang til telefonen. Husk \u00e5 <a class\="pwm-link-print"> skrive ut denne siden</a> eller p\u00e5 annen m\u00e5te skrive ned disse kodene og lagre dem p\u00e5 et trygt sted.
+Display_OtpClearWarning=Er du sikker p\u00e5 at du vil fortsette? Hvis du fortsetter, blir din eksisterende registrering slettet, og du m\u00e5 konfigurere enheten p\u00e5 nytt.
+Display_ResponsesClearWarning=Er du sikker p\u00e5 at du vil fortsette? Hvis du fortsetter, blir din eksisterende svar slettet, og du m\u00e5 registrere nye svar p\u00e5 sikkerhetssp\u00f8rsm\u00e5lene.
+Display_Shortcuts=Velg en av f\u00f8lgende lenker for \u00e5 fortsette.
+Display_ShowPasswordGuide=Passordguide
+Display_StrengthMeter=Passordstyrke
+Display_UpdateProfile=Vennligst oppdater f\u00f8lgende informasjon\:
+Display_UpdateProfileConfirm=Vennligst les f\u00f8lgende informasjon du har lagt inn, og bekreft denne.
+Display_UpdateProfileEnterCode=For \u00e5 verifisere din epostadresse s\u00e5 har en kode blitt sendt til deg p\u00e5 <b>%1%</b>.  Oppgi koden for \u00e5 fortsette.
+Display_UpdateProfileEnterCodeSMS=For \u00e5 verifisere ditt telefonnummer s\u00e5 har en kode blitt sendt til deg p\u00e5 <b>%1%</b>.  Oppgi koden for \u00e5 fortsette.
+Display_UserEventHistory=Denne siden viser passordhistorikken din. Bare handlinger utf\u00f8rt via denne selvbetjeningsportalen er vist her. Alle tidspunkt er opgitt i %1% tidssone.
+Display_TypingWait=Venter p\u00e5 inntasting for \u00e5 fullf\u00f8re ....
+Field_AccountEnabled=Konto aktivisert
+Field_AccountExpired=Konto utl\u00f8pt
+Field_AccountExpirationTime=Utl\u00f8pstid p\u00e5 konto
+Field_Code=Kode
+Field_DateTime=Dato/tid
+Field_OneTimePassword=Engangspassord
+Field_Confirm_Prefix=Bekreft
+Field_ConfirmPassword=Bekreft passord
+Field_CurrentPassword=N\u00e5v\u00e6rende passord
+Field_Display=Vis
+Field_ForwardURL=Videresendings-URL
+Field_LastLoginTime=Sist gang logget inn
+Field_LastLoginTimeDelta=Last Login Time Delta
+Field_LdapProfile=LDAP profil
+Field_Location=Lokasjon
+Field_LogoutURL=URL ved utlogging
+Field_Method=Metode
+Field_NetworkAddress=Network Address
+Field_NetworkHost=Network Host
+Field_NewPassword=Nytt passord
+Field_Option_Select=Velg et sp\u00f8rsm\u00e5l
+Field_Password=Passord
+Field_PasswordExpirationTime=Utl\u00f8pstid p\u00e5 passord
+Field_PasswordExpired=Passord utl\u00f8pt
+Field_PasswordLocked=Passord sperret (Intruder Detect)
+Field_PasswordPreExpired=Passord forutl\u00f8pt
+Field_PasswordSetTime=Tid for opprettelse av passord
+Field_PasswordSetTimeDelta=Password Set Time Delta
+Field_PasswordViolatesPolicy=Bryter passordreglene
+Field_PasswordWithinWarningPeriod=Innen venteperioden
+Field_Policy=Regel
+Field_Profile=Profil
+Field_ResponsesNeeded=Svaroppdateringer er n\u00f8dvendig
+Field_ResponsesStored=Svar lagret
+Field_ResponsesTimestamp=Stored Responses Timestamp
+Field_User_Supplied_Question=Sp\u00f8rsm\u00e5l
+Field_UserDN=Bruker DN
+Field_UserGUID=Bruker GUID
+Field_Username=Brukernavn
+Field_UserEmail=Epost
+Field_UserSMS=SMS
+Field_OTP_Identifier=Identifikator
+Field_OTP_Secret=Hemmelig
+Field_OTP_Type=Type
+Field_OTP_RecoveryCodes=Gjenopprettingskoder
+Field_OTP_Stored=OTP lagret
+Field_OTP_Timestamp=OTP lagringstid
+Field_VerificationMethodPreviousAuth=Forrige autentisering
+Field_VerificationMethodToken=SMS/epost verifisering
+Field_VerificationMethodOTP=Verifisering fra mobilenhet
+Field_VerificationMethodChallengeResponses=Fortrolige sp\u00f8rsm\u00e5l og svar
+Field_VerificationMethodAttributes=Personlig informasjon
+Field_VerificationMethodRemoteResponses=Eksterne svar
+Field_VerificationMethodNAAF=Advanced Authentication
+Field_VerificationMethodOAuth=Ekstern OAuth autentisering
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
+Field_Placeholder_Answer=Svar
+Long_Title_ActivateUser=Aktiviser en forh\u00e5ndskonfigurert konto og etabler et nytt passord.
+Long_Title_Admin=Administrative funksjoner
+Long_Title_ChangePassword=Endre ditt n\u00e5v\u00e6rende passord.
+Long_Title_ForgottenPassword=F\u00e5 tilgang til kontoen din igjen hvis du har glemt passordet.
+Long_Title_ForgottenUsername=Finn glemt brukernavn.
+Long_Title_GuestRegistration=Registrer en ny gjestebrukerkonto.
+Long_Title_GuestUpdate=Oppdatere en ny gjestebrukerkonto.
+Long_Title_Helpdesk=Helpdesk verkt\u00f8y
+Long_Title_Logout=Logg ut fra passordselvbetjeningen.
+Long_Title_Main_Menu=Hovedmeny for passordselvbetjening. Herfra kan du endre ditt n\u00e5v\u00e6rende passord, lage nytt passord n\u00e5r du har glemt ditt gamle eller utf\u00f8re andre passordrelaterte aktiviteter
+Long_Title_NewUser=Opprett en ny brukerkonto.
+Long_Title_PeopleSearch=Finn kontaktinformasjon til dine kolleger.
+Long_Title_SetupResponses=Lag passordhint for glemt passord. Disse hemmelige sp\u00f8rsm\u00e5lene og svarene vil gj\u00f8re det mulig \u00e5 lage nytt passord hvis du glemmer det gamle.
+Long_Title_SetupOtpSecret=Oppsett mobilappenhet. Hvis du glemmer passordet ditt, kan du bruke mobilenheten til \u00e5 autentisere til dette nettstedet.
+Long_Title_Shortcuts=Personlige snarveier.
+Long_Title_UpdateProfile=Oppdater dine brukerprofilopplysninger.
+Long_Title_UserEventHistory=Passordhistorikk. Se oversikt over n\u00e5r du har endret passordet ditt.
+Long_Title_UserInformation=Passordhistorikk. Se oversikt over n\u00e5r du har endret passordet ditt.
+Long_Title_DeleteAccount=Fjern din konto og profil fra denne tjenesten
+Long_Title_VerificationSend=F\u00f8r denne brukeren kan velges, m\u00e5 brukerens identitet bekreftes. Vennligst velg en bekreftelsesmetode.
+Title_AnsweredQuestions=Besvarte sp\u00f8rsm\u00e5l
+Title_ActivateUser=Aktiviser konto
+Title_Admin=Administrator
+Title_Captcha=Verifisering
+Title_ChangePassword=Endre passord
+Title_ConfirmResponses=Bekreft sikkerhetssp\u00f8rsm\u00e5l
+Title_Error=Feil
+Title_ForgottenPassword=Glemt passord
+Title_ForgottenUsername=Glemt brukernavn
+Title_GuestRegistration=Gjesteregistrering
+Title_GuestUpdate=Oppdater gjestebruker
+Title_Helpdesk=Helpdesk
+Title_LocaleSelect=Spr\u00e5kvalg
+Title_Login=Logg inn
+Title_Logout=Logg ut
+Title_LogoutPublic=Inaktiv timeout
+Title_MainPage=Hovedmeny
+Title_NewUser=Registrering av ny bruker
+Title_OrgChart=Organisasjonskart
+Title_PasswordGuide=Passordguide
+Title_PasswordPolicy=Passordregel
+Title_PasswordStrength=Passordstyrke
+Title_PasswordWarning=Passordadvarsel
+Title_PeopleSearch=S\u00f8ke etter personer
+Title_PeopleSearchCard=Persons\u00f8k - kort
+Title_PeopleSearchTable=Persons\u00f8k - tabell
+Title_PleaseWait=Vennligst vent
+Title_RandomPasswords=Tilfeldig passord
+Title_RecentVerifications=Nylige verifikasjoner
+Title_RecoverPassword=Glemt passord
+Title_RecoverRandomResponses=Tilfeldige sp\u00f8rsm\u00e5l
+Title_RecoverRequiredResponses=P\u00e5krevede sp\u00f8rsm\u00e5l
+Title_SecurityResponses=Sikkerhetssvar
+Title_SetupRandomResponses=Tilfeldige sp\u00f8rsm\u00e5l
+Title_SetupRequiredResponses=P\u00e5krevede sp\u00f8rsm\u00e5l
+Title_SetupResponses=Oppgi passordhint
+Title_SetupOtpSecret=Konfigurer mobilapp autentisering
+Title_Shortcuts=Snarveier
+Title_Status=Status
+Title_Success=Suksess
+Title_UpdateProfile=Oppdater profil
+Title_UpdateProfileConfirm=Bekreft profilinnhold
+Title_UserData=Min informasjon
+Title_UserEventHistory=Passordhistorie
+Title_UserInformation=Min konto
+Title_ValidateCode=Verifiseringskode
+Title_VerificationSend=Velg verifiseringsmetode
+Title_DeleteAccount=Slett min konto
+Title_Management=Administrasjon
+Title_DirectReports=Rapporterer til
+Title_Organization=Organisasjon
+Tooltip_PasswordStrength=Passordkvalitetsm\u00e5leren viser hvor lett det er \u00e5 gjette<br/> passordet ditt. Pr\u00f8v f\u00f8lgende for \u00e5 gj\u00f8re passordet ditt sikrere:<ul><li>Gj\u00f8r passordet lengre</li><li>Ikke gjenta bokstaver eller tall</li><li>Bruk b\u00e5de store og sm\u00e5 bokstaver</li><li>Legg til flere tall</li><li>Legg til flere spesialtegn</li></ul>
+Confirm_DeleteUser=Er du sikker p\u00e5 at du vil fortsette? Hvis du fortsetter, slettes den valgte brukeren permanent. Dette kan ikke angres.
+Confirm=Er du sikker p\u00e5 at du vil fortsette?
+Value_False=Usant
+Value_True=Sant
+Value_NotApplicable=n/a
+Value_Default=Standard
+Value_ProgressComplete=Ferdig
+Value_ProgressInProgress=I prosess
+Placeholder_Search=S\u00f8k

+ 172 - 0
server/src/main/resources/password/pwm/i18n/Error_nb.properties

@@ -0,0 +1,172 @@
+#=
+# Password Management Servlets (PWM)
+# http://www.pwm-project.org
+#=
+# Copyright (c) 2006-2009 Novell, Inc.
+# Copyright (c) 2009-2017 The PWM Project
+#=
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#=
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#=
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#=
+
+Password_MissingConfirm=Passordet oppfyller kravene, vennligst bekreft passordet
+Password_DoesNotMatch=Passordene samsvarer ikke
+Password_Missing=Passord mangler
+Password_PreviouslyUsed=Nytt passord har blitt brukt tidligere
+Password_BadPassword=Nytt passord oppfyller ikke kravene
+Password_TooLong=Nytt passord er for langt
+Password_TooShort=Nytt passord er for kort
+Password_NotEnoughNum=Nytt passord har ikke nok siffer
+Password_NotEnoughAlpha=Nytt passord har ikke nok bokstaver
+Password_NotEnoughUpper=Nytt passord har ikke nok store bokstaver
+Password_NotEnoughLower=Nytt passord har ikke nok sm\u00e5 bokstaver
+Password_NotEnoughSpecial=Nytt passord har ikke nok spesialtegn (som ikke er bokstaver eller tall)
+Password_NotEnoughUnique=Nytt passord har ikke nok unike karakterer
+Password_TooManyRepeat=Nytt passord har for mange gjentatte tegn
+Password_TooManySpecial=Nytt passord har for mange spesialtegn (som ikke er bokstaver eller tall)
+Password_TooManyAlpha=Nytt passord har for mange bokstaver
+Password_TooManyNumeric=Nytt passord har for mange siffer
+Password_TooManyLower=Nytt passord har for mange sm\u00e5 bokstaver
+Password_TooManyUpper=Nytt passord har for mange store bokstaver
+Password_TooManyOldChars=Nytt passord inneholder for mange tegn fra det gamle passordet
+Password_TooManyNonAlphaSpecial=Nytt passord har for mange spesialtegn (som ikke er bokstaver eller tall)
+Password_InvalidChar=Nytt passord har et ugyldig tegn
+Password_RequiredMissing=Nytt passord mangler et n\u00f8dvendig tegn
+Password_InWordlist=Nytt passord er altfor vanlig
+Password_SameAsOld=Nytt passord er det samme som gjeldende passord
+Password_SameAsAttr=Nytt passord er for opplagt
+Password_FirstIsNumeric=Det f\u00f8rste tegnet m\u00e5 v\u00e6re numerisk
+Password_LastIsNumeric=Den siste tegnet kan ikke v\u00e6re numerisk
+Password_FirstIsSpecial=Det f\u00f8rste tegnet m\u00e5 v\u00e6re et spesialtegn (som ikke er bokstaver eller tall)
+Password_LastIsSpecial=Den siste tegnet kan ikke v\u00e6re et spesialtegn (som ikke er bokstaver eller tall)
+Password_HistoryFull=Passordhistorikken er full
+Password_MeetsRules=Nytt passord godtatt, vennligst klikk endre passord
+Password_TooSoon=Det har ikke g\u00e5tt nok tid siden forrige endring av passord
+Password_UsingDisallowedValue=Nytt passord bruker en verdi som ikke er tillatt
+Password_TooWeak=Passordet er for svakt. Pr\u00f8v \u00e5 legge til flere tall, spesialtegn eller blandede bokstaver.
+Password_TooManyNonAlpha=Nytt passord har for mange tegn som ikke er bokstaver
+Password_NotEnoughNonAlpha=Nytt passord har ikke nok tegn som ikke er bokstaver
+Password_UnknownValidation=Nytt passord oppfyller ikke kravene. Pr\u00f8v \u00e5 bruke et annet passord.
+Password_NewPasswordRequired=Nytt passord er p\u00e5krevet f\u00f8r du kan fortsette.
+Password_Expired=Passordet har utl\u00f8pt.
+Password_CustomError=Nytt passord oppfyller ikke kravene
+Password_BadOldPassword=Det gamle passordet er feil
+Password_NotEnoughGroups=Nytt passord inneholder ikke nok forskjellige typer tegn
+Password_TooManyConsecutive=Nytt passord har for mange p\u00e5f\u00f8lgende tegn (for eksempel 123456... eller abcdef...)
+
+Error_WrongResponse=En eller flere svar er feil. Vennligst pr\u00f8v igjen.
+Error_WrongPassword=Brukernavnet eller passord er ikke gyldig. Vennligst pr\u00f8v igjen.
+Error_Response_NoResponse=Brukernavnet er enten ikke gyldig eller kan ikke benytte denne funksjonen
+Error_Response_TooShort=Svaret p\u00e5 sp\u00f8rsm\u00e5let "%1%" er for kort
+Error_Response_Wordlist=Svaret p\u00e5 sp\u00f8rsm\u00e5let "%1%" er benyttet for ofte
+Error_Response_TooLong=Svaret p\u00e5 sp\u00f8rsm\u00e5let "%1%" er for langt
+Error_Response_Duplicate=Svaret p\u00e5 sp\u00f8rsm\u00e5let "%1%" kan ikke v\u00e6re det samme som et annet svar
+Error_Challenge_Duplicate=Hvert sp\u00f8rsm\u00e5l m\u00e5 v\u00e6re unikt.
+Error_Missing_Challenge_Text=Det mangler tekst til sp\u00f8rsm\u00e5let fra brukeren.
+Error_UserAuthenticated=Du er allerede godkjent.
+Error_MissingParameter=En p\u00e5krevd parameter mangler.
+Error_DirectoryUnavailable=Katalogen er utilgjengelig. Hvis denne feilen oppst\u00e5r gjentatte ganger kan du kontakte brukerst\u00f8tte.
+Error_Unknown=Ukjent feil. Hvis denne feilen oppst\u00e5r gjentatte ganger kan du kontakte brukerst\u00f8tte.
+Error_ActivationValidationFailed=En eller flere verdier er ikke korrekte.
+Error_CantMatchUser=Kan ikke finne brukernavnet. Vennligst pr\u00f8v igjen.
+Error_ServiceNotAvailable=Tjenesten er ikke aktivert.
+Error_UserMisMatch=Autentisering feilet, lukk nettleseren.
+Error_ActivateUserNoQueryMatch=Din brukerkonto er ikke kvalifisert for aktivering.
+Error_AuthenticationRequired=Autentisering n\u00f8dvendig.
+Error_NoChallenges=Ingen sp\u00f8rsm\u00e5l har blitt konfigurert.
+Error_UserIntruder=Maks p\u00e5loggingsfors\u00f8k for bruker-IDen din er overskredet. Pr\u00f8v igjen senere.
+Error_AddressIntruder=Maks p\u00e5loggingsfors\u00f8k har blitt overskredet. Pr\u00f8v igjen senere.
+Error_SessionIntruder=Maks p\u00e5loggingsfors\u00f8k for denne \u00f8kten er overskredet.
+Error_BadSessionPassword=Kan ikke opprette sesjons-passord.
+Error_Unauthorized=Du har ikke tillatelse til \u00e5 utf\u00f8re den valgte handlingen.
+Error_BadSession=Kan ikke opprette en \u00f8kt med nettleseren din. Vennligst lukke nettleseren og pr\u00f8v igjen.
+Error_MissingRequiredResponse=Vennligst oppgi alle de n\u00f8dvendige svarene.
+Error_MissingRandomResponse=Vennligst oppgi et ekstra tilfeldig svar.
+Error_BadCaptchaResponse=Feil bekreftelseskode, pr\u00f8v igjen.
+Error_CaptchaAPIError=En feil oppstod under validering av CAPTCHA svar. Vennligst lukk nettleser og pr\u00f8v igjen. Hvis denne feilen oppst\u00e5r gjentatte ganger s\u00e5 ta kontakt med brukerst\u00f8tte.\n
+Error_InvalidConfig=Denen konfigurasjonen av portalprogrammet er ugyldig eller skadet. Vennligst korriger feilen, eller fjerne konfigurasjonsfilen.\n
+Error_InvalidFormID=Nettlesersesjonen er ugyldig eller er utl\u00f8pt. Pr\u00f8v p\u00e5 nytt.
+Error_TokenMissingContact=Det er ingen kontaktinformasjon tilgjengelig for kontoen din. Vennligst kontakt din administrator.
+Error_TokenIncorrect=Feil kode, vennligst pr\u00f8v igjen.
+Error_BadCurrentPassword=N\u00e5v\u00e6rende passord er feil, vennligst pr\u00f8v igjen.
+Error_Closing=Operasjonen kan ikke fullf\u00f8re fordi portalen avsluttes.
+Error_Missing_GUID=Kan ikke finne en GUID for bruker. Vennligst kontakt din administrator.
+Error_TokenExpired=Koden du har tastet inn er utl\u00f8pt, og er ikke lenger gyldig. Vennligst pr\u00f8v igjen.
+Error_SecureRequestRequired=Usikrede (HTTP) tilkoblinger er ikke tillatt til dette systemet. Vennligst pr\u00f8v igjen ved hjelp av en sikker (HTTPS) tilkobling.
+Error_Writing_Responses=En feil oppstod under lagring av svaret p\u00e5 ditt sp\u00f8rsm\u00e5l. Vennligst kontakt din administrator.
+Error_Writing_Otp_Secret=Det oppsto en feil under lagringen av OTP-informasjonen. Ta kontakt med administratoren din.
+Error_Unlock_Failure=En feil oppstod under oppl\u00e5sning av kontoen din. Vennligst kontakt din administrator.
+Error_Update_Attrs_Failure=En feil oppstod under lagring av profilen din. Vennligst kontakt din administrator.
+Error_Activation_Failure=En feil oppstod under aktivering av kontoen. Vennligst kontakt din administrator.
+Error_NewUser_Failure=En feil oppsto mens du laget ny brukerkonto. Vennligst kontakt din administrator.
+Error_Activation=Kan ikke aktivere kontoen din med den informasjonen du har oppgitt. Vennligst pr\u00f8v igjen.
+Error_DB_Unavailable=Database utilgjengelig. Hvis denne feilen oppst\u00e5r gjentatte ganger kan du kontakte brukerst\u00f8tte.
+Error_LocalDB_Unavailable=Databasen til portalprogrammet er utilgjengelig. Hvis denne feilen oppst\u00e5r gjentatte ganger kan du kontakte brukerst\u00f8tte.
+Error_App_Unavailable=Portal-programmet er ikke tilgjengelig eller starter p\u00e5 nytt. Hvis denne feilen oppst\u00e5r gjentatte ganger kan du kontakte brukerst\u00f8tte.
+Error_IncorrectRequestSequence=Bruk av tilbake-knappen eller flere webleser-sesjoner har blitt oppdaget. Vennligst lukk alle nettlesere og pr\u00f8v igjen.
+Error_UnreachableCloudService=En ekstern tjeneste var utilgjengelig.
+Error_InvalidSecurityKey=Portalprogrammets sikkerhetsn\u00f8kkel mangler eller er ugyldig.
+Error_Clearing_Responses=En feil oppstod under godkjenning av responsen p\u00e5 sp\u00f8rsm\u00e5lene. Vennligst kontakt din administrator.
+Error_ServiceUnreachable=A required service is unavailable.  Please try again later.
+Error_ChallengeInResponse=Svaret p\u00e5 "%1%" kan ikke inneholde deler av sp\u00f8rsm\u00e5let.
+Error_Multi_Username=Flere brukere samsvarer med det oppgitte brukernavnet "%1%". Vennligst avgrens s\u00f8ket ditt.
+Error_CertificateError=En sertifikatefeil oppstod: %1%.
+Error_SyslogWriteError=A problem writing to the syslog server has been encountered, error: %1%
+Error_TooManyThreads=Maximum thread count limit exceeded, please try again later
+Error_SecurityViolation=A security violation has occurred.  Please try again later.
+Error_NoOtpConfiguration=Ingen engangspassord er konfigurert.
+Error_TrialViolation=Trial limits have been exceeded.
+Error_AccountDisabled=Konto er deaktiver
+Error_AccountExpired=Konto har utl\u00f8pt.
+Error_WrongOtpToken=Feil engangspassord.
+Error_AttrIntruder=Maksimale s\u00f8kefors\u00f8k er overskredet. Pr\u00f8v igjen senere.
+Error_AuditWrite=Klarte ikke \u00e5 skrive audit loggen.
+Error_LdapIntruder=Maks p\u00e5loggingsfors\u00f8k for bruker-IDen din er overskredet. Pr\u00f8v igjen senere.
+Error_NoLdapConnection=A connection to the required directory is not available.
+Error_OAuthError=En feil ved bruk av OAuth-autentiseringsprotokollen har oppst\u00e5tt. Pr\u00f8v igjen senere.
+Error_FieldRequired=%1% p\u00e5krevet
+Error_FieldNotANumber=%1% m\u00e5 v\u00e6re tall
+Error_FieldInvalidEmail=%1% er ikke en gyldig e-postadresse
+Error_FieldTooShort=%1% er for kort
+Error_FieldTooLong=%1% er for langt
+Error_FieldDuplicate=%1% er allerede brukt, vennligst bruk en annen verdi
+Error_FieldBadConfirm=%1% felt samsvarer ikke
+Error_FieldRegexNoMatch=%1% er ikke korrekt format
+Error_Orig_Admin_Only=Bare den opprinnelige administratoren kan utf\u00f8re denne egenskapen
+Error_PasswordRequired=Det kreves et passord for \u00e5 utf\u00f8re denne operasjonen
+Error_ReportingError=An error during report generation occurred
+Error_TokenDestIntruder=Maksimale fors\u00f8k er overskredet. Pr\u00f8v igjen senere.
+Error_OtpRecoveryUsed=Gjenopprettingen kunne v\u00e6rt tidligere brukt og kan ikke brukes igjen.
+Error_RedirectIllegal=The requested redirect url is not permitted.
+Error_CryptError=An unexpected cryptography error has occurred.
+Error_SmsSendError=Kan ikke sende sms-melding: %1%
+Error_LdapDataError=An LDAP data error has occurred.
+Error_MacroParseError=Macro parse error: %1%
+Error_NoProfileAssigned=Ingen profil er tildelt for denne operasjonen.
+Error_StartupError=An error occurred while starting the application.  Check the log files for information.
+Error_EnvironmentError=An error with the application environment has prevented the application from starting.
+Error_ApplicationNotRunning=Denne funksjonaliteten er ikke tilgjengelig f\u00f8r programkonfigurasjonen er beskyttet.
+Error_EmailSendFailure=Kunne ikke sende eposten %1%, feil: %2%
+Error_PasswordOnlyBad=Passord er feil. Pr\u00f8v p\u00e5 nytt.
+
+Error_RemoteErrorValue=Remote Error: %1%
+
+Error_ConfigUploadSuccess=Fil er lastet opp
+Error_ConfigUploadFailure=Fil kunne ikke lastes opp.
+Error_ConfigSaveSuccess=Konfigurasjon lagret. Omstart av portalprogrammet har blitt forespurt. Portalprogrammet kan v\u00e6re utilgjengelig mens maskinen starter. Hvis omstart ikke kreves kan det hende du m\u00e5 starte portalprogrammet manuelt.\n
+Error_ConfigFormatError=Konfigurasjon formatfeil: %1%
+Error_ConfigLdapFailure=Kunne ikke koble til LDAP katalogtjener.
+Error_ConfigLdapSuccess=Tilkoblet til LDAP katalogtjener var vellykket
+
+Error_HTTP_404=Siden du forespurte ble ikke funnet.

+ 173 - 0
server/src/main/resources/password/pwm/i18n/Message_nb.properties

@@ -0,0 +1,173 @@
+#=
+# Password Management Servlets (PWM)
+# http://www.pwm-project.org
+#=
+# Copyright (c) 2006-2009 Novell, Inc.
+# Copyright (c) 2009-2017 The PWM Project
+#=
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#=
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#=
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#=
+
+EventLog_Startup=Application Startup
+EventLog_Shutdown=Application Shutdown
+EventLog_FatalEvent=Fatal Event
+EventLog_ModifyConfiguration=Configuration Modified
+EventLog_IntruderAttempt=Intruder Attempt
+EventLog_IntruderLockout=Intruder Lockout
+EventLog_ActivateUser=Aktiviser konto
+EventLog_Authenticate=Authentication
+EventLog_AgreementPassed=Agreement Passed
+EventLog_ChangePassword=Endre passord
+EventLog_UnlockPassword=L\u00e5s opp passord
+EventLog_CreateUser=Create Account
+EventLog_HelpdeskAction=Help Desk Action
+EventLog_HelpdeskClearResponses=Help Desk Clear Responses
+EventLog_HelpdeskClearOtpSecret=Help Desk Clear OTP Secret
+EventLog_HelpdeskSetPassword=Help Desk Set Password
+EventLog_HelpdeskUnlockPassword=Help Desk Unlock Password
+EventLog_HelpdeskDeleteUser=Help Desk Delete User
+EventLog_HelpdeskViewDetail=Help Desk View Detail
+EventLog_HelpdeskVerifyOtp=Help Desk Verify OTP
+EventLog_HelpdeskVerifyOtpIncorrect=Help Desk Incorrect OTP Verify
+EventLog_HelpdeskVerifyToken=Help Desk Verify Token
+EventLog_HelpdeskVerifyTokenIncorrect=Help Desk Incorrect Token Verify
+EventLog_HelpdeskVerifyAttributes=Help Desk Verify Attributes
+EventLog_HelpdeskVerifyAttributesIncorrect=Help Desk Incorrect Attributes Verify
+EventLog_IntruderUserAttempt=Intruder User Attempt
+EventLog_IntruderUserLock=Intruder User Lockout
+EventLog_TokenIssued=Token Issued
+EventLog_TokenClaimed=Token Claimed
+EventLog_ClearResponses=Clear Responses
+EventLog_RecoverPassword=Recover Forgotten Password
+EventLog_SetupResponses=Setup Password Responses
+Eventlog_SetupOtpSecret=Setup OTP Secret
+EventLog_UpdateProfile=Oppdater attributter
+EventLog_DeleteAccount=Delete Account
+Requirement_ADComplexity=M\u00e5 ha minst tre typer av f\u00f8lgende tegn: <ul><li>store (A-Z)</li><li>sm\u00e5 bokstaver (a-z)</li><li>tall (0-9)</li><li>Symbol (!, #, $, etc.)</li></ul>
+Requirement_ADComplexity2008=M\u00e5 ha minst %1% typer av f\u00f8lgende tegn: <ul><li>store (A-Z)</li><li>sm\u00e5 bokstaver (a-z)</li><li>tall (0-9)</li><li>Symbol (!, #, $, etc.)</li><li>Andre spr\u00e5ktegn som ikke er oppf\u00f8rt ovenfor</li></ul>
+Requirement_AllowNumeric=M\u00e5 ikke inneholde numeriske tegn.
+Requirement_AllowSpecial=M\u00e5 ikke inneholde noen spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_CaseSensitive=Passord ser forskjell p\u00e5 store/sm\u00e5 tegn.
+Requirement_DisAllowedAttributes=M\u00e5 ikke inkludere deler av navnet ditt eller brukernavn.
+Requirement_DisAllowedValues=M\u00e5 ikke inneholde noen av f\u00f8lgende verdier: %1%.
+Requirement_FirstNumeric=Det f\u00f8rste tegnet kan ikke v\u00e6re numerisk.
+Requirement_FirstSpecial=Det f\u00f8rste tegnet kan ikke v\u00e6re et spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_LastNumeric=Det siste tegnet kan ikke v\u00e6re numerisk.
+Requirement_LastSpecial=Den siste tegnet kan ikke v\u00e6re et spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_MaxAlpha=Kan ikke ha med mer enn %1% bokstav.
+Requirement_MaxAlphaPlural=Kan ikke ha med mer enn %1% bokstaver.
+Requirement_MaxLength=M\u00e5 ikke v\u00e6re mer enn %1% tegn langt.
+Requirement_MaxLengthPlural=M\u00e5 ikke v\u00e6re mer enn %1% tegn langt.
+Requirement_MaxLower=Kan ikke ha med mer enn %1% liten (sm\u00e5) bokstav(er).
+Requirement_MaxLowerPlural=Kan ikke ha med mer enn %1% liten (sm\u00e5) bokstav(er).
+Requirement_MaxNumeric=Kan ikke ha med mer enn %1% tall.
+Requirement_MaxNumericPlural=Kan ikke ha med mer enn %1% tall.
+Requirement_MaxRepeat=Ingen tegn m\u00e5 gjetas mer enn %1% gang.
+Requirement_MaxRepeatPlural=Ingen tegn m\u00e5 gjetas mer enn %1% ganger.
+Requirement_MaxSeqRepeat=Ingen tegn m\u00e5 gjentas sekvensielt mer enn %1% gang.
+Requirement_MaxSeqRepeatPlural=Ingen tegn m\u00e5 gjentas sekvensielt mer enn %1% ganger.
+Requirement_MaxSpecial=Kan ikke ha mer enn %1% spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_MaxSpecialPlural=Kan ikke ha mer enn %1% spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_MaxUpper=Kan ikke ha med mer enn %1% storbokstav.
+Requirement_MaxUpperPlural=Kan ikke ha med mer enn %1% stor(e) bokstav(er).
+Requirement_MinAlpha=M\u00e5 ha med minst %1% bokstav.
+Requirement_MinAlphaPlural=M\u00e5 ha med minst %1% bokstaver.
+Requirement_MinimumFrequency=Kan ikke endres oftere enn en gang hver %1%.
+Requirement_MinLength=M\u00e5 v\u00e6re minst %1% tegn langt.
+Requirement_MinLengthPlural=M\u00e5 v\u00e6re minst %1% tegn langt.
+Requirement_MinLower=M\u00e5 ha minst %1% liten (sm\u00e5) bokstav.
+Requirement_MinLowerPlural=M\u00e5 ha minst %1% sm\u00e5 bokstav.
+Requirement_MinNumeric=M\u00e5 ha med minst %1% tall.
+Requirement_MinNumericPlural=M\u00e5 ha med minst %1% tall.
+Requirement_MinSpecial=M\u00e5 ha minst %1% spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_MinSpecialPlural=M\u00e5 ha minst %1% spesialtegn (noe annet enn bokstaver eller tall).
+Requirement_MinUnique=M\u00e5 ha minst %1% unik karakter.
+Requirement_MinUniquePlural=M\u00e5 ha minst %1% unike karakterer.
+Requirement_MinUpper=M\u00e5 ha minst %1% stor bokstav.
+Requirement_MinUpperPlural=M\u00e5 ha minst %1% store bokstaver.
+Requirement_NotCaseSensitive=Passord ser ikke forskjell p\u00e5 store/sm\u00e5 tegn.
+Requirement_OldChar=Kan ikke ha mer enn %1% karakter fra ditt n\u00e5v\u00e6rende passord.
+Requirement_OldCharPlural=Kan ikke ha mer enn %1% karakterer fra ditt n\u00e5v\u00e6rende passord.
+Requirement_RequiredChars=M\u00e5 inkludere minst et av hvert av de f\u00f8lgende tegn:
+Requirement_UniqueRequired=Nytt passord kan ikke ha v\u00e6rt brukt tidligere.
+Requirement_WordList=M\u00e5 ikke inkludere et vanlig ord eller en vanlig sekvens av tegn.
+Rule_PolicyEnabled=Regel aktivert
+Rule_MinimumLength=Minimume lengde
+Rule_MaximumLength=Maksimum lengde
+Rule_MinimumUpperCase=Minimum store tegn
+Rule_MaximumUpperCase=Maksimum store tegn
+Rule_MinimumLowerCase=Minimum sm\u00e5 tegn
+Rule_MaximumLowerCase=maksimum sm\u00e5 tegn
+Rule_AllowNumeric=Tillat numeriske tegn
+Rule_MinimumNumeric=Minimum numeriske tegn
+Rule_MaximumNumeric=Maksimum numeriske tegn
+Rule_MinimumUnique=Minimum unike
+Rule_MaximumUnique=Maksimum unike
+Rule_AllowFirstCharNumeric=Tillat f\u00f8rste tegn \u00e5 v\u00e6re numerisk
+Rule_AllowLastCharNumeric=Tillat siste tegn \u00e5 v\u00e6re numerisk
+Rule_AllowSpecial=Tillat spesialtegn
+Rule_MinimumSpecial=Minimum spesialtegn
+Rule_MaximumSpecial=Maksimum spesialtegn
+Rule_AllowFirstCharSpecial=Tillat f\u00f8rste tegn \u00e5 v\u00e6re spesial
+Rule_AllowLastCharSpecial=Tillat siste tegn \u00e5 v\u00e6re spesial
+Rule_MaximumRepeat=Maksimum gentagelse
+Rule_MaximumSequentialRepeat=Maksimum sekvensiell gjentakelse
+Rule_ChangeMessage=Endre melding
+Rule_ExpirationInterval=Intervall for utl\u00f8p
+Rule_MinimumLifetime=Minimum levetid
+Rule_CaseSensitive=Store/sm\u00e5 sensitive
+Rule_EnforceAtLogin=Tving ved p\u00e5logging
+Rule_ChallengeResponseEnabled=Spr\u00f8sm\u00e5l/svar aktivert
+Rule_UniqueRequired=Unik p\u00e5krevet
+Rule_DisallowedValues=Ugyldige verdier
+Rule_DisallowedAttributes=Ugyldige attributter
+Rule_ADComplexity=AD kompleksitet
+Rule_DisallowCurrent=Ugyldig n\u00e5v\u00e6rende
+Rule_MaximumOldChars=Maksimum tidligere tegn
+Rule_RegExMatch=Regular Expression Match
+Rule_RegExNoMatch=Regular Expression No Match
+Rule_MinimumAlpha=Minimum bokstaver
+Rule_MaximumAlpha=Maksimum bokstaver
+Rule_MinimumNonAlpha=Minimum ikke-bokstaver
+Rule_MaximumNonAlpha=Maksimum ikke-bokstaver
+Rule_EnableWordlist=Aktiver ordliste
+Rule_MinimumStrength=Minimum styrke
+Rule_MaximumConsecutive=Maksimum p\u00e5f\u00f8lgende
+Rule_CharGroupsValues=Verdier for tegngrupper
+Rule_CharGroupsMinMatch=Tegngrupper minimum p\u00e5krevet
+Rule_AllowUserChange=Tillat endring av passord for Admin
+Rule_AllowAdminChange=Tillat endring av passord for bruker
+Rule_ADComplexityLevel=AD kompleksitetsniv\u00e5
+Rule_ADComplexityMaxViolations=Maksimum AD kompleksitetsbrudd
+Success_ActivateUser=Din brukerkonto har blitt aktivert. S\u00f8rg for \u00e5 fullf\u00f8re prosessen, ellers vil du ikke kunne f\u00e5 tilgang til kontoen din.
+Success_ConfigFileUpload=Konfigurasjonsfilen har blitt lastet opp.
+Success_ClearResponse=Dine sp\u00f8rsm\u00e5l og svar har blitt fjernet.
+Success_CreateGuest=Den nye gjestekontoen er opprettet. Gjestebrukeren vil f\u00e5 en melding til e-postadressen som er angitt. Gjestekontoen kan fortsatt trenge aktivisering.
+Success_CreateUser=Din nye brukerkonto er opprettet.
+Success_NewUserForm=Din konto er klar til \u00e5 bli opprettet. Fortsett n\u00e5r du er klar.
+Success_PasswordChange=Passordendringen var vellykket.
+Success_ChangedHelpdeskPassword=Passordet har blitt endret for bruker %1%.
+Success_PasswordReset=Passordet for %1% er satt.
+Success_PasswordSend=Ditt nye passord har blitt sendt til %1%. Lukk dette vinduet og logg inn med ditt nye passord.
+Success_ResponsesMeetRules=Dine svar oppfyller kravene. Klikk Lagre svar n\u00e5r du er klar.
+Success_SetupResponse=Dine hemmelige sp\u00f8rsm\u00e5l og svar har blitt lagret. Hvis du glemmer passordet ditt, kan du bruke svarene p\u00e5 disse sp\u00f8rsm\u00e5lene for \u00e5 tilbakestille passordet.\n
+Success_Unknown=Operasjonen er fullf\u00f8rt.
+Success_UnlockAccount=Din konto har blitt l\u00e5st opp.
+Success_UpdateGuest=Gjestekontoen har blitt oppdatert. Gjestebrukeren vil f\u00e5 en melding til e-postadressen som er angitt.
+Success_UpdateProfile=Din brukerinformasjonen har blitt oppdatert.
+Success_UpdateForm=Din profil kan oppdateres. Fortsett n\u00e5r du er klar.
+Success_Action=Operasjonen %1% er utf\u00f8rt.
+Success_OtpSetup=Enheten er registrert.
+Success_TokenResend=En ny sikkerhetskode er sendt til deg.

+ 2 - 0
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -554,6 +554,7 @@ Setting_Description_password.sharedHistory.age=Specify the maximum age of the sh
 Setting_Description_password.sharedHistory.enable=Enable this option to use a global shared password history for all users.  If enabled, all users share a common password history.  This helps prevent users from using common organizational words as passwords.  @PwmAppName@ stores the passwords as a salted and encrypted hash in the LocalDB.
 Setting_Description_password.showAutoGen=Enable this option to display a link to users during the change password process that displays a list of auto-generated sample passwords that the system allows through the configured password policies.  The users have the option to select and use one of the values in the list.  If this option does not force the user to choose a password from the list.
 Setting_Description_password.showStrengthMeter=Enable this option to allow the users to see the password strength meter on the change password screen.
+Setting_Description_password.strengthMeter.type=Choose the calculation algorithm type used for the password strength meter.
 Setting_Description_passwordSync.enableReplicaCheck=Enable this option to check for the password to be updated on all configured replicas (for a user's LDAP Profile).  When enabled, replica sync checking polls all of the configured replicas on the user's LDAP Profile to determine if the password change time has been updated.  The particular method to determine the last password change time varies per LDAP vendor type.
 Setting_Description_passwordSyncMaxWaitTime=Specify how long, during a password change, the system waits for the password to be synchronized to all configured LDAP servers.  In cases where the synchronization might take an extraordinary amount of time, this setting prevents the page from timing out.<br/><br/>Specify the value in seconds.
 Setting_Description_passwordSyncMinWaitTime=Specify how long, during a password change, the system waits before forwarding the user.  This gives any background synchronization processes time to execute before the user executes the next operation.<br/><br/>Specify the value in seconds.
@@ -1038,6 +1039,7 @@ Setting_Label_password.sharedHistory.age=Shared History Age
 Setting_Label_password.sharedHistory.enable=Enable Shared History
 Setting_Label_password.showAutoGen=Show Auto Generate Randoms
 Setting_Label_password.showStrengthMeter=Show Strength Meter
+Setting_Label_password.strengthMeter.type=Strength Meter Algorithm
 Setting_Label_passwordSync.enableReplicaCheck=Password Sync Enable Replication Checking
 Setting_Label_passwordSyncMaxWaitTime=Password Change Maximum Wait Time
 Setting_Label_passwordSyncMinWaitTime=Password Change Minimum Wait Time

+ 18 - 1
server/src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp

@@ -238,9 +238,13 @@
                         </td>
                     </tr>
                     <% for (final AppDashboardData.ServiceData loopService : appDashboardData.getServices()) { %>
-                    <tr>
+                    <tr id="serviceName-<%=loopService.getName()%>">
                         <td>
                             <%= loopService.getName() %>
+                            <% if (!JavaHelper.isEmpty(loopService.getDebugData())) { %>
+                            &nbsp;
+                            <div class="btn-icon pwm-icon pwm-icon-list-alt"></div>
+                            <% } %>
                         </td>
                         <td>
                             <%= loopService.getStatus() %>
@@ -430,6 +434,19 @@
                     }})
                 });
             });
+            <% for (final AppDashboardData.ServiceData loopService : appDashboardData.getServices()) { %>
+            <% if (!JavaHelper.isEmpty(loopService.getDebugData())) { %>
+            PWM_MAIN.addEventHandler('serviceName-<%=loopService.getName()%>','click',function(){
+                var tableText = '<table>';
+                <% for (final Map.Entry<String,String> entry : loopService.getDebugData().entrySet()) { %>
+                tableText += '<tr><td><%=StringUtil.escapeJS(entry.getKey())%></td>'
+                + '<td><%=StringUtil.escapeJS(entry.getValue())%></td></tr>';
+                <% } %>
+                tableText += '</table>';
+                PWM_MAIN.showDialog({title:'Debug Properties',text:tableText});
+            });
+            <% } %>
+            <% } %>
         });
     </script>
 </pwm:script>

+ 2 - 1
server/src/main/webapp/WEB-INF/jsp/configguide-ldap_schema.jsp

@@ -41,7 +41,8 @@
     boolean existingSchemaGood = false;
     String schemaActivityLog = "";
     try {
-        final SchemaOperationResult schemaManager = ConfigGuideUtils.extendSchema(configGuideBean,false);
+        final PwmApplication pwmApplication = JspUtility.getPwmRequest(pageContext).getPwmApplication();
+        final SchemaOperationResult schemaManager = ConfigGuideUtils.extendSchema(pwmApplication, configGuideBean, false);
         existingSchemaGood = schemaManager.isSuccess();
         schemaActivityLog = schemaManager.getOperationLog();
     } catch (Exception e) {

+ 12 - 14
server/src/main/webapp/WEB-INF/jsp/error.jsp

@@ -50,20 +50,18 @@
         <span id="message" class="message message-error"><pwm:ErrorMessage/></span>
         <br/>
         <br/>
-        <pwm:if test="<%=PwmIfTest.showErrorDetail%>">
-            <% if (errorInformation != null && !errorInformation.getError().isErrorIsPermanent()) { %>
-            <div class="buttonbar">
-                <form action="<pwm:url url='<%=PwmServletDefinition.PublicCommand.servletUrl()%>' addContext="true"/>" method="post" enctype="application/x-www-form-urlencoded">
-                    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=CommandServlet.CommandAction.next.toString()%>"/>
-                    <button type="submit" name="button" class="btn" id="button_continue" autofocus="autofocus">
-                        <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
-                        <pwm:display key="Button_Continue"/>
-                    </button>
-                    <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
-                </form>
-            </div>
-            <% } %>
-        </pwm:if>
+        <% if (errorInformation != null && !errorInformation.getError().isErrorIsPermanent()) { %>
+        <div class="buttonbar">
+            <form action="<pwm:url url='<%=PwmServletDefinition.PublicCommand.servletUrl()%>' addContext="true"/>" method="post" enctype="application/x-www-form-urlencoded">
+                <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=CommandServlet.CommandAction.next.toString()%>"/>
+                <button type="submit" name="button" class="btn" id="button_continue" autofocus="autofocus">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
+                    <pwm:display key="Button_Continue"/>
+                </button>
+                <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+        </div>
+        <% } %>
     </div>
     <div class="push"></div>
 </div>

+ 53 - 374
server/src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp

@@ -20,49 +20,18 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   --%>
 
-<%@ page import="com.novell.ldapchai.ChaiPasswordRule" %>
-<%@ page import="com.novell.ldapchai.cr.Challenge" %>
-<%@ page import="password.pwm.bean.ResponseInfoBean" %>
-<%@ page import="password.pwm.bean.pub.PublicUserInfoBean" %>
-<%@ page import="password.pwm.config.PwmSetting" %>
-<%@ page import="password.pwm.config.option.HelpdeskUIMode" %>
-<%@ page import="password.pwm.config.option.ViewStatusFields" %>
-<%@ page import="password.pwm.config.profile.HelpdeskProfile" %>
-<%@ page import="password.pwm.config.profile.PwmPasswordRule" %>
-<%@ page import="password.pwm.config.value.data.ActionConfiguration" %>
-<%@ page import="password.pwm.config.value.data.FormConfiguration" %>
-<%@ page import="password.pwm.http.PwmSession" %>
 <%@ page import="password.pwm.http.servlet.helpdesk.HelpdeskDetailInfoBean" %>
-<%@ page import="password.pwm.http.tag.PasswordRequirementsTag" %>
-<%@ page import="password.pwm.i18n.Display" %>
-<%@ page import="password.pwm.svc.event.UserAuditRecord" %>
 <%@ page import="password.pwm.util.java.JavaHelper" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
-<%@ page import="password.pwm.util.java.TimeDuration" %>
-<%@ page import="password.pwm.util.macro.MacroMachine" %>
 <%@ page import="java.time.Instant" %>
-<%@ page import="java.util.Iterator" %>
-<%@ page import="java.util.List" %>
-<%@ page import="java.util.Set" %>
-<%@ page import="org.apache.commons.text.StringEscapeUtils" %>
+<%@ page import="password.pwm.http.bean.DisplayElement" %>
+<%@ page import="password.pwm.http.servlet.accountinfo.AccountInformationBean" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html; charset=UTF-8" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
-<%
-    final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext);
-    final PwmSession pwmSession = pwmRequest.getPwmSession();
-    final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-    final HelpdeskProfile helpdeskProfile = pwmSession.getSessionManager().getHelpdeskProfile(pwmApplication);
-    final HelpdeskUIMode SETTING_PW_UI_MODE = HelpdeskUIMode.valueOf(helpdeskProfile.readSettingAsString(PwmSetting.HELPDESK_SET_PASSWORD_MODE));
+<% final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext); %>
+<% final HelpdeskDetailInfoBean helpdeskDetailInfoBean = (HelpdeskDetailInfoBean)pwmRequest.getAttribute(PwmRequestAttribute.HelpdeskDetail); %>
 
-    // user info
-    final HelpdeskDetailInfoBean helpdeskDetailInfoBean = (HelpdeskDetailInfoBean)pwmRequest.getAttribute(PwmRequestAttribute.HelpdeskDetail);
-    final PublicUserInfoBean searchedUserInfo = helpdeskDetailInfoBean.getUserInfo();
-    final ResponseInfoBean responseInfoBean = helpdeskDetailInfoBean.getResponseInfoBean();
-
-    final String displayName = helpdeskDetailInfoBean.getUserDisplayName();
-    final Set<ViewStatusFields> viewStatusFields = helpdeskProfile.readSettingAsOptionList(PwmSetting.HELPDESK_VIEW_STATUS_VALUES,ViewStatusFields.class);
-%>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
 <body class="nihilo">
@@ -72,14 +41,14 @@
     </jsp:include>
     <div id="centerbody" style="min-width: 800px">
         <div id="page-content-title"><pwm:display key="Title_Helpdesk" displayIfMissing="true"/></div>
-        <% if (displayName != null && !displayName.isEmpty()) { %>
-        <h2 style="text-align: center"><%=displayName%></h2>
+        <% if (!StringUtil.isEmpty(helpdeskDetailInfoBean.getUserDisplayName())) { %>
+        <h2 style="text-align: center"><%=StringUtil.escapeHtml(helpdeskDetailInfoBean.getUserDisplayName())%></h2>
         <% } %>
         <pwm:script>
             <script type="text/javascript">
                 PWM_GLOBAL['startupFunctions'].push(function(){
                     PWM_VAR["helpdesk_obfuscatedDN"] = '<%=JspUtility.getAttribute(pageContext, PwmRequestAttribute.HelpdeskObfuscatedDN)%>';
-                    PWM_VAR["helpdesk_username"] = '<%=StringUtil.escapeJS((String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.HelpdeskUsername))%>';
+                    PWM_VAR["helpdesk_username"] = '<%=helpdeskDetailInfoBean.getUserDisplayName()%>';
                 });
             </script>
         </pwm:script>
@@ -91,313 +60,18 @@
                         <div id="Field_Profile" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Field_Profile"/>" class="tabContent">
                             <div style="max-height: 400px; overflow: auto;">
                                 <table class="nomargin">
-                                    <% for (final FormConfiguration formItem : helpdeskDetailInfoBean.getSearchDetails().keySet()) { %>
-                                    <tr>
-                                        <td class="key" id="key_<%=StringUtil.escapeHtml(formItem.getName())%>" title="<%=StringUtil.escapeHtml(formItem.getDescription(pwmRequest.getLocale()))%>">
-                                            <%= formItem.getLabel(pwmSession.getSessionStateBean().getLocale())%>
-                                        </td>
-                                        <td id="value_<%=formItem.getName()%>">
-                                            <% for (final Iterator<String> iter = helpdeskDetailInfoBean.getSearchDetails().get(formItem).iterator(); iter.hasNext(); ) { %>
-                                            <% final String loopValue = iter.next(); %>
-                                            <%= loopValue == null ? "" : StringUtil.escapeHtml(loopValue) %>
-                                            <% if (iter.hasNext()) { %> <br/> <% } %>
-                                            <% } %>
-                                        </td>
-                                    </tr>
-                                    <%  } %>
+                                    <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getProfileData()) { %>
+                                    <% request.setAttribute("displayElement", displayElement); %>
+                                    <jsp:include page="fragment/displayelement-row.jsp"/>
+                                    <% } %>
                                 </table>
                             </div>
                         </div>
                         <div id="Title_Status" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_Status"/>" class="tabContent">
                             <table class="nomargin">
-                                <% if (viewStatusFields.contains(ViewStatusFields.UserDN)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_UserDN"/>
-                                    </td>
-                                    <td>
-                                        <span style="word-wrap: break-word; word-break: break-all">
-                                        <%= StringUtil.escapeHtml(searchedUserInfo.getUserDN()) %>
-                                        </span>
-                                    </td>
-                                </tr>
-                                <% if (pwmApplication.getConfig().getLdapProfiles().size() > 1) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_LdapProfile"/>
-                                    </td>
-                                    <td>
-                                        <%= StringUtil.escapeHtml(pwmApplication.getConfig().getLdapProfiles().get(searchedUserInfo.getLdapProfile()).getDisplayName(pwmSession.getSessionStateBean().getLocale())) %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.Username)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_Username"/>
-                                    </td>
-                                    <td>
-                                        <%= StringUtil.escapeHtml(searchedUserInfo.getUserID()) %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.UserEmail)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_UserEmail"/>
-                                    </td>
-                                    <td>
-                                        <% if (searchedUserInfo.getUserEmailAddress() == null) { %>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                        <% } else { %>
-                                        <%= StringUtil.escapeHtml(searchedUserInfo.getUserEmailAddress()) %>
-                                        <% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.UserSMS)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_UserSMS"/>
-                                    </td>
-                                    <td>
-                                        <% if (searchedUserInfo.getUserSmsNumber() == null) { %>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                        <% } else { %>
-                                        <%= StringUtil.escapeHtml(searchedUserInfo.getUserSmsNumber()) %>
-                                        <% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.AccountEnabled)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_AccountEnabled"/>
-                                    </td>
-                                    <td>
-                                        <%if (helpdeskDetailInfoBean.isAccountEnabled()) { %><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.AccountExpired)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_AccountExpired"/>
-                                    </td>
-                                    <td>
-                                        <%if (helpdeskDetailInfoBean.isAccountExpired()) { %><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.AccountExpirationTime)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_AccountExpirationTime"/>
-                                    </td>
-                                    <% if (searchedUserInfo.getAccountExpirationTime() == null) { %>
-                                    <td>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td class="timestamp">
-                                        <%= JavaHelper.toIsoDate(searchedUserInfo.getAccountExpirationTime()) %>
-                                    </td>
-                                    <% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.LastLoginTime)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_LastLoginTime"/>
-                                    </td>
-                                    <% if (helpdeskDetailInfoBean.getLastLoginTime() == null) { %>
-                                    <td>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td class="timestamp">
-                                        <%= JavaHelper.toIsoDate(helpdeskDetailInfoBean.getLastLoginTime()) %>
-                                    </td>
-                                    <% } %>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.LastLoginTimeDelta)) { %>
-                                <% if (helpdeskDetailInfoBean.getLastLoginTime() != null) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_LastLoginTimeDelta"/>
-                                    </td>
-                                    <td>
-                                        <%= TimeDuration.fromCurrent(helpdeskDetailInfoBean.getLastLoginTime()).asLongString(pwmSession.getSessionStateBean().getLocale()) %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordExpired)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordExpired"/>
-                                    </td>
-                                    <td>
-                                        <%if (searchedUserInfo.getPasswordStatus().isExpired()) {%><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordPreExpired)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordPreExpired"/>
-                                    </td>
-                                    <td>
-                                        <%if (searchedUserInfo.getPasswordStatus().isPreExpired()) {%><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordWarnPeriod)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordWithinWarningPeriod"/>
-                                    </td>
-                                    <td>
-                                        <%if (searchedUserInfo.getPasswordStatus().isWarnPeriod()) { %><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordSetTime)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordSetTime"/>
-                                    </td>
-                                    <% if (searchedUserInfo.getPasswordLastModifiedTime() == null) { %>
-                                    <td>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td class="timestamp">
-                                        <%= JavaHelper.toIsoDate(searchedUserInfo.getPasswordLastModifiedTime()) %>
-                                    </td>
-                                    <% } %>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordSetTimeDelta)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordSetTimeDelta"/>
-                                    </td>
-                                    <td>
-                                        <%= helpdeskDetailInfoBean.getPasswordSetDelta() %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.PasswordExpireTime)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordExpirationTime"/>
-                                    </td>
-                                    <% if (searchedUserInfo.getPasswordExpirationTime() == null) { %>
-                                    <td>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td class="timestamp">
-                                        <%= JavaHelper.toIsoDate(searchedUserInfo.getPasswordExpirationTime()) %>
-                                    </td>
-                                    <% } %>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.IntruderDetect)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_PasswordLocked"/>
-                                    </td>
-                                    <% if (helpdeskDetailInfoBean.isIntruderLocked()) { %>
-                                    <td class="health-WARN">
-                                        <pwm:display key="Value_True"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td>
-                                        <pwm:display key="Value_False"/>
-                                    </td>
-                                    <% } %>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.ResponsesStored)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_ResponsesStored"/>
-                                    </td>
-                                    <td>
-                                        <% if (responseInfoBean != null) { %>
-                                        <pwm:display key="Value_True"/>
-                                        <% } else { %>
-                                        <pwm:display key="Value_False"/>
-                                        <% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.ResponsesNeeded)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_ResponsesNeeded"/>
-                                    </td>
-                                    <td>
-                                        <% if (searchedUserInfo.isRequiresResponseConfig()) { %>
-                                        <pwm:display key="Value_True"/>
-                                        <% } else { %>
-                                        <pwm:display key="Value_False"/>
-                                        <% } %>
-                                    </td>
-                                </tr>
-                                <% } %>
-                                <% if (viewStatusFields.contains(ViewStatusFields.ResponsesTimestamp)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_ResponsesTimestamp"/>
-                                    </td>
-                                    <% if (responseInfoBean == null || responseInfoBean.getTimestamp() == null) { %>
-                                    <td>
-                                        <pwm:display key="Value_NotApplicable"/>
-                                    </td>
-                                    <% } else { %>
-                                    <td class="timestamp">
-                                        <%= JavaHelper.toIsoDate(responseInfoBean.getTimestamp()) %>
-                                    </td>
-                                    <% } %>
-                                </tr>
-                                <% } %>
-                                <pwm:if test="<%=PwmIfTest.otpEnabled%>">
-                                    <% if (viewStatusFields.contains(ViewStatusFields.OTPStored)) { %>
-                                    <tr>
-                                        <td class="key">
-                                            <pwm:display key="Field_OTP_Stored"/>
-                                        </td>
-                                        <td>
-                                            <%if (helpdeskDetailInfoBean.isHasOtpRecord()) {%><pwm:display key="Value_True"/><% } else { %><pwm:display key="Value_False"/><% } %>
-                                        </td>
-                                    </tr>
-                                    <% } %>
-                                    <% if (viewStatusFields.contains(ViewStatusFields.OTPTimestamp)) { %>
-                                    <tr>
-                                        <td class="key">
-                                            <pwm:display key="Field_OTP_Timestamp"/>
-                                        </td>
-                                        <td class="timestamp">
-                                            <%= helpdeskDetailInfoBean.getOtpRecordTimestamp() %>
-                                        </td>
-                                    </tr>
-                                    <% } %>
-                                </pwm:if>
-                                <% if (viewStatusFields.contains(ViewStatusFields.GUID)) { %>
-                                <tr>
-                                    <td class="key">
-                                        <pwm:display key="Field_UserGUID"/>
-                                    </td>
-                                    <td>
-                                        <%= StringUtil.escapeHtml(searchedUserInfo.getUserGUID()) %>
-                                    </td>
-                                </tr>
+                                <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getStatusData()) { %>
+                                <% request.setAttribute("displayElement", displayElement); %>
+                                <jsp:include page="fragment/displayelement-row.jsp"/>
                                 <% } %>
                             </table>
                         </div>
@@ -405,14 +79,13 @@
                         <div id="Title_UserEventHistory" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_UserEventHistory"/>" class="tabContent">
                             <div style="max-height: 400px; overflow: auto;">
                                 <table class="nomargin">
-                                    <% for (final UserAuditRecord record : helpdeskDetailInfoBean.getUserHistory()) { %>
+                                    <% for (final AccountInformationBean.ActivityRecord record : helpdeskDetailInfoBean.getUserHistory()) { %>
                                     <tr>
                                         <td class="key timestamp" style="width:50%">
                                             <%= JavaHelper.toIsoDate(record.getTimestamp()) %>
                                         </td>
                                         <td>
-                                            <%= record.getEventCode().getLocalizedString(pwmRequest.getConfig(), pwmRequest.getLocale()) %>
-                                            <%= record.getMessage() != null && record.getMessage().length() > 1 ? " (" + record.getMessage() + ") " : "" %>
+                                            <%= record.getLabel() %>
                                         </td>
                                     </tr>
                                     <% } %>
@@ -467,18 +140,12 @@
                                 </table>
                             </div>
                         </div>
-                        <% if (responseInfoBean != null && responseInfoBean.getHelpdeskCrMap() != null && !responseInfoBean.getHelpdeskCrMap().isEmpty()) { %>
+                        <% if (!JavaHelper.isEmpty(helpdeskDetailInfoBean.getHelpdeskResponses())) { %>
                         <div id="Title_SecurityResponses" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_SecurityResponses"/>" class="tabContent">
                             <table class="nomargin">
-                                <% for (final Challenge challenge : responseInfoBean.getHelpdeskCrMap().keySet()) { %>
-                                <tr>
-                                    <td class="key">
-                                        <%=challenge.getChallengeText()%>
-                                    </td>
-                                    <td>
-                                        <%=responseInfoBean.getHelpdeskCrMap().get(challenge)%>
-                                    </td>
-                                </tr>
+                                <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getHelpdeskResponses()) { %>
+                                <% request.setAttribute("displayElement", displayElement); %>
+                                <jsp:include page="fragment/displayelement-row.jsp"/>
                                 <% } %>
                             </table>
                         </div>
@@ -489,23 +156,31 @@
                 </td>
                 <td class="noborder" style="width: 200px; max-width:200px; text-align: left; vertical-align: top">
                     <div class="noborder" style="margin-top: 25px; margin-left: 5px">
-                        <button name="button_continue" class="helpdesk-detail-btn btn" id="button_continue" autofocus>
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.back)) { %>
+                        <button name="they" class="helpdesk-detail-btn btn" id="button_continue" autofocus>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
                             <pwm:display key="Button_GoBack"/>
                         </button>
+                        <% } %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.refresh)) { %>
                         <button name="button_refresh" class="helpdesk-detail-btn btn" id="button_refresh">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-refresh"></span></pwm:if>
                             <pwm:display key="Display_CaptchaRefresh"/>
                         </button>
+                        <% } %>
+
                         <br/><br/>
-                        <% if (SETTING_PW_UI_MODE != HelpdeskUIMode.none) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.changePassword)) { %>
                         <button class="helpdesk-detail-btn btn" id="helpdesk_ChangePasswordButton">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-key"></span></pwm:if>
                             <pwm:display key="Button_ChangePassword"/>
                         </button>
                         <% } %>
-                        <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_ENABLE_UNLOCK)) { %>
-                        <% if (helpdeskDetailInfoBean.isIntruderLocked()) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.unlock)) { %>
+                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.unlock)) { %>
                         <button id="helpdesk_unlockBtn" class="helpdesk-detail-btn btn">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-unlock"></span></pwm:if>
                             <pwm:display key="Button_Unlock"/>
@@ -517,8 +192,9 @@
                         </button>
                         <% } %>
                         <% } %>
-                        <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_CLEAR_RESPONSES_BUTTON)) { %>
-                        <% if (responseInfoBean != null) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearResponses)) { %>
+                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearResponses)) { %>
                         <button id="helpdesk_clearResponsesBtn" class="helpdesk-detail-btn btn">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
                             <pwm:display key="Button_ClearResponses"/>
@@ -535,13 +211,13 @@
                                         id: "helpdesk_clearResponsesBtn",
                                         text: 'User does not have responses'
                                     });
-                                });
-                            </script>
+                                });</script>
                         </pwm:script>
                         <% } %>
                         <% } %>
-                        <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_CLEAR_OTP_BUTTON) && pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.OTP_ENABLED)) { %>
-                        <% if (helpdeskDetailInfoBean.isHasOtpRecord()) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearOtpSecret)) { %>
+                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearOtpSecret)) { %>
                         <button id="helpdesk_clearOtpSecretBtn" class="helpdesk-detail-btn btn">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
                             <pwm:display key="Button_HelpdeskClearOtpSecret"/>
@@ -553,19 +229,20 @@
                         </button>
                         <% } %>
                         <% } %>
-                        <% if ((Boolean)JspUtility.getPwmRequest(pageContext).getAttribute(PwmRequestAttribute.HelpdeskVerificationEnabled) == true) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.verification)) { %>
                         <button id="sendTokenButton" class="helpdesk-detail-btn btn">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-mobile-phone"></span></pwm:if>
                             <pwm:display key="Button_Verify"/>
                         </button>
                         <% } %>
-                        <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_DELETE_USER_BUTTON)) { %>
+
+                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.deleteUser)) { %>
                         <button class="helpdesk-detail-btn btn" id="helpdesk_deleteUserButton">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-user-times"></span></pwm:if>
                             <pwm:display key="Button_Delete"/>
                         </button>
                         <% } %>
-                        <% final List<ActionConfiguration> actions = helpdeskProfile.readSettingAsAction(PwmSetting.HELPDESK_ACTIONS); %>
 
                         <button id="loadDetail" style="display:none">Load Detail</button>
                         <pwm:script>
@@ -575,7 +252,7 @@
                                         var url = 'helpdesk';
                                         url = PWM_MAIN.addParamToUrl(url, 'processAction', 'detail');
                                         url = PWM_MAIN.addParamToUrl(url, 'userKey', PWM_VAR['helpdesk_obfuscatedDN']);
-                                        url = PWM_MAIN.addParamToUrl(url, 'verificationState', PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE));
+                                        //url = PWM_MAIN.addParamToUrl(url, 'verificationState', PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE));
                                         PWM_MAIN.ajaxRequest(url,function () {
                                         });
                                     });
@@ -583,26 +260,28 @@
                             </script>
                         </pwm:script>
 
-                        <% for (final ActionConfiguration loopAction : actions) { %>
-                        <button class="helpdesk-detail-btn btn" name="action-<%=loopAction.getName()%>" id="action-<%=loopAction.getName()%>">
+                        <% if (!JavaHelper.isEmpty(helpdeskDetailInfoBean.getCustomButtons())) { %>
+                        <% for (final HelpdeskDetailInfoBean.ButtonInfo customButton : helpdeskDetailInfoBean.getCustomButtons()) { %>
+                        <button class="helpdesk-detail-btn btn" name="action-<%=customButton.getName()%>" id="action-<%=customButton.getName()%>">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-location-arrow"></span></pwm:if>
-                            <%=StringUtil.escapeHtml(loopAction.getName())%>
+                            <%=StringUtil.escapeHtml(customButton.getLabel())%>
                         </button>
                         <pwm:script>
                             <script type="text/javascript">
                                 PWM_GLOBAL['startupFunctions'].push(function(){
-                                    PWM_MAIN.addEventHandler('action-<%=loopAction.getName()%>','click',function(){
-                                        PWM_HELPDESK.executeAction('<%=StringUtil.escapeJS(loopAction.getName())%>');
+                                    PWM_MAIN.addEventHandler('action-<%=customButton.getName()%>','click',function(){
+                                        PWM_HELPDESK.executeAction('<%=StringUtil.escapeJS(customButton.getName())%>');
                                     });
                                     PWM_MAIN.showTooltip({
-                                        id: "action-<%=loopAction.getName()%>",
+                                        id: "action-<%=customButton.getName()%>",
                                         position: 'above',
-                                        text: '<%=StringUtil.escapeJS(loopAction.getDescription())%>'
+                                        text: '<%=StringUtil.escapeJS(customButton.getDescription())%>'
                                     });
                                 });
                             </script>
                         </pwm:script>
                         <% } %>
+                        <% } %>
                     </div>
                 </td>
             </tr>

+ 7 - 3
server/src/main/webapp/public/resources/js/changepassword.js

@@ -150,12 +150,16 @@ PWM_CHANGEPW.markStrength = function(strength) { //strength meter
     var strengthLabel = "";
     var barColor = "";
 
-    if (strength > 70) {
+    if (strength == 100) {
+        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryHigh');
+    } else if (strength >= 75) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthHigh');
-    } else if (strength > 45) {
+    } else if (strength >= 45) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthMedium');
-    } else {
+    } else if (strength >= 20) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthLow');
+    } else {
+        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryLow');
     }
 
     var colorFade = function(h1, h2, p) { return ((h1>>16)+((h2>>16)-(h1>>16))*p)<<16|(h1>>8&0xFF)+((h2>>8&0xFF)-(h1>>8&0xFF))*p<<8|(h1&0xFF)+((h2&0xFF)-(h1&0xFF))*p; }

+ 61 - 51
server/src/main/webapp/public/resources/js/main.js

@@ -25,6 +25,11 @@
 var PWM_GLOBAL = PWM_GLOBAL || {};
 var PWM_MAIN = PWM_MAIN || {};
 var PWM_VAR = PWM_VAR || {};
+var PWM_API = PWM_API || {};
+
+PWM_API.formatDate = function(dateObj) {
+    return PWM_MAIN.TimestampHandler.formatDate(dateObj);
+};
 
 PWM_MAIN.ajaxTimeout = 120 * 1000;
 
@@ -240,8 +245,8 @@ PWM_MAIN.applyFormAttributes = function() {
             PWM_MAIN.addEventHandler(linkElement, "click", function (event) {
                 event.preventDefault();
                 PWM_MAIN.showWaitDialog({loadFunction: function () {
-                    PWM_MAIN.goto(hrefValue);
-                }});
+                        PWM_MAIN.goto(hrefValue);
+                    }});
             });
             linkElement.removeAttribute('href');
         }
@@ -333,8 +338,8 @@ PWM_MAIN.goto = function(url,options) {
         executeGoto();
     } else {
         PWM_MAIN.showWaitDialog({loadFunction:function () {
-            executeGoto();
-        }});
+                executeGoto();
+            }});
     }
 };
 
@@ -345,38 +350,38 @@ PWM_MAIN.handleLoginFormSubmit = function(form, event) {
 
     require(["dojo","dojo/dom-form"], function(dojo, domForm) {
         PWM_MAIN.showWaitDialog({loadFunction: function () {
-            var options = {};
-            options['content'] = domForm.toObject(form);
-            delete options['content']['processAction'];
-            delete options['content']['pwmFormID'];
-            var url = 'login?processAction=restLogin&skipCaptcha=' + options['content']['skipCaptcha'];
-            var loadFunction = function(data) {
-                if (data['error'] === true) {
-                    PWM_MAIN.getObject('password').value = '';
-                    PWM_MAIN.showErrorDialog(data,{
-                        okAction:function(){
-                            setTimeout(function(){
-                                PWM_MAIN.getObject('password').focus();
-                            },50);
-                        }
-                    });
-                    return;
-                }
-                console.log('authentication success');
-                var nextURL = data['data']['nextURL'];
-                if (nextURL) {
-                    PWM_MAIN.goto(nextURL, {noContext: true});
-                }
-            };
-            PWM_MAIN.ajaxRequest(url,loadFunction,options);
-            if(typeof grecaptcha !== 'undefined'){
-                try {
-                    grecaptcha.reset(); // reset the
-                } catch (e) {
-                    console.log("error resetting the captcha: " + e)
+                var options = {};
+                options['content'] = domForm.toObject(form);
+                delete options['content']['processAction'];
+                delete options['content']['pwmFormID'];
+                var url = 'login?processAction=restLogin&skipCaptcha=' + options['content']['skipCaptcha'];
+                var loadFunction = function(data) {
+                    if (data['error'] === true) {
+                        PWM_MAIN.getObject('password').value = '';
+                        PWM_MAIN.showErrorDialog(data,{
+                            okAction:function(){
+                                setTimeout(function(){
+                                    PWM_MAIN.getObject('password').focus();
+                                },50);
+                            }
+                        });
+                        return;
+                    }
+                    console.log('authentication success');
+                    var nextURL = data['data']['nextURL'];
+                    if (nextURL) {
+                        PWM_MAIN.goto(nextURL, {noContext: true});
+                    }
+                };
+                PWM_MAIN.ajaxRequest(url,loadFunction,options);
+                if(typeof grecaptcha !== 'undefined'){
+                    try {
+                        grecaptcha.reset(); // reset the
+                    } catch (e) {
+                        console.log("error resetting the captcha: " + e)
+                    }
                 }
-            }
-        }});
+            }});
     });
 };
 
@@ -395,8 +400,8 @@ PWM_MAIN.handleFormSubmit = function(form, event) {
     }
 
     PWM_MAIN.showWaitDialog({loadFunction:function(){
-        form.submit();
-    }});
+            form.submit();
+        }});
     return false;
 };
 
@@ -584,7 +589,7 @@ PWM_MAIN.showLocaleSelectionMenu = function(nextFunction, options) {
 
     };
 
-    var bodyHtml = '<table class="noborder" style="width:auto;margin-right:auto;margin-left:auto">';
+    var bodyHtml = '<table class="noborder" style="width:auto;margin-right:auto;margin-left:auto;overflow-x:scroll">';
     localeIterator(function(localeKey){
         if (!PWM_MAIN.JSLibrary.arrayContains(excludeLocales, localeKey)) {
             var loopDisplayName = localeData[localeKey];
@@ -1652,13 +1657,11 @@ PWM_MAIN.TimestampHandler.initAllElements = function() {
 
 PWM_MAIN.TimestampHandler.testIfStringIsTimestamp = function(input, trueFunction) {
     if (input && input.length > 0) {
-        require(["dojo", "dojo/date/stamp"], function (dojo, IsoDate) {
-            input = dojo.trim(input);
-            var dateObj = IsoDate.fromISOString(input);
-            if (dateObj) {
-                trueFunction(dateObj);
-            }
-        });
+        input = input.trim();
+        var timestamp = Date.parse(input);
+        if (isNaN(timestamp) === false) {
+            trueFunction(new Date(timestamp));
+        }
     }
 };
 
@@ -1673,10 +1676,10 @@ PWM_MAIN.TimestampHandler.initElement = function(element) {
 
     require(["dojo"], function(dojo) {
         var innerText = dojo.attr(element, 'innerHTML');
-        innerText = dojo.trim(innerText);
-        PWM_MAIN.TimestampHandler.testIfStringIsTimestamp(innerText, function (dateObj) {
+        innerText = innerText.trim(innerText);
+        PWM_MAIN.TimestampHandler.testIfStringIsTimestamp(innerText, function () {
             element.setAttribute('data-timestamp-original', innerText);
-            PWM_MAIN.addEventHandler(element.id, 'click', function(){
+            PWM_MAIN.addEventHandler(element, 'click', function(){
                 var LocalizedState = !PWM_MAIN.Preferences.readSessionStorage(PWM_MAIN.TimestampHandler.PreferencesKey,true);
                 PWM_MAIN.Preferences.writeSessionStorage(PWM_MAIN.TimestampHandler.PreferencesKey, LocalizedState);
                 PWM_MAIN.TimestampHandler.updateAllElements();
@@ -1704,12 +1707,13 @@ PWM_MAIN.TimestampHandler.updateAllElements = function() {
 };
 
 PWM_MAIN.TimestampHandler.updateElement = function(element) {
-    require(["dojo","dojo/date/stamp","dojo/date/locale"], function(dojo,IsoDate,LocaleDate) {
+    require(["dojo"], function(dojo) {
         var localized = PWM_MAIN.Preferences.readSessionStorage(PWM_MAIN.TimestampHandler.PreferencesKey,true);
         if (localized) {
             var isoDateStr = element.getAttribute('data-timestamp-original');
-            var date = IsoDate.fromISOString(isoDateStr);
-            var localizedStr = LocaleDate.format(date,{formatLength:'long'});
+            var date = new Date(Date.parse(isoDateStr));
+            var localizedStr = PWM_MAIN.TimestampHandler.formatDate(date);
+
             dojo.attr(element,'innerHTML',localizedStr);
         } else {
             dojo.attr(element,'innerHTML',element.getAttribute('data-timestamp-original'));
@@ -1717,6 +1721,12 @@ PWM_MAIN.TimestampHandler.updateElement = function(element) {
     })
 };
 
+PWM_MAIN.TimestampHandler.formatDate = function(dateObj) {
+    var options = {timeZoneName:'short', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'};
+    var locale = PWM_GLOBAL['client.locale'];
+    return dateObj.toLocaleString(locale, options);
+};
+
 PWM_MAIN.addPwmFormIDtoURL = function(url) {
     return PWM_MAIN.addParamToUrl(url,'pwmFormID',PWM_GLOBAL['pwmFormID']);
 };

+ 7 - 3
server/src/main/webapp/public/resources/js/newuser.js

@@ -118,12 +118,16 @@ PWM_NEWUSER.markStrength=function(strength) { //strength meter
     var strengthLabel = "";
     var barColor = "";
 
-    if (strength > 70) {
+    if (strength == 100) {
+        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryHigh');
+    } else if (strength >= 75) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthHigh');
-    } else if (strength > 45) {
+    } else if (strength >= 45) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthMedium');
-    } else {
+    } else if (strength >= 20) {
         strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthLow');
+    } else {
+        strengthLabel = PWM_MAIN.showString('Display_PasswordStrengthVeryLow');
     }
 
     var colorFade = function(h1, h2, p) { return ((h1>>16)+((h2>>16)-(h1>>16))*p)<<16|(h1>>8&0xFF)+((h2>>8&0xFF)-(h1>>8&0xFF))*p<<8|(h1&0xFF)+((h2&0xFF)-(h1&0xFF))*p; }

+ 4 - 0
server/src/main/webapp/public/resources/style.css

@@ -1264,3 +1264,7 @@ html[dir="rtl"] .errorDetail {
 html[dir="rtl"] .message.message-error .errorDetail {
     display: inline-block;
 }
+
+#strengthLabel {
+    white-space: nowrap;
+}

+ 19 - 11
server/src/test/java/password/pwm/tests/PwmPasswordJudgeTest.java

@@ -24,6 +24,11 @@ package password.pwm.tests;
 
 import junit.framework.Assert;
 import junit.framework.TestCase;
+import org.mockito.Mockito;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.StrengthMeterType;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.operations.PasswordUtility;
 
 import java.util.ArrayList;
@@ -31,20 +36,23 @@ import java.util.List;
 
 public class PwmPasswordJudgeTest extends TestCase {
     public void testJudgePassword() throws Exception {
-        Assert.assertEquals(0, PasswordUtility.judgePasswordStrength(""));
-        Assert.assertEquals(100, PasswordUtility.judgePasswordStrength(
+        final Configuration configuration = Mockito.mock(Configuration.class);
+        Mockito.when(configuration.readSettingAsEnum(PwmSetting.PASSWORD_STRENGTH_METER_TYPE, StrengthMeterType.class)).thenReturn(StrengthMeterType.PWM);
+
+        Assert.assertEquals(0, PasswordUtility.judgePasswordStrength(configuration,""));
+        Assert.assertEquals(100, PasswordUtility.judgePasswordStrength(configuration,
                 "V.{a$f.*B697e+%J9pOPn~E0CyqN~9XmR?yjOGFC(k+la?n6&^I3bwZq[miF(`0"));
 
         final List<Integer> judgeValues = new ArrayList<>();
-        judgeValues.add(PasswordUtility.judgePasswordStrength(""));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasd"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdA"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdAASDSADSAD"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdAASDSADSAD#"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdAASDSADSAD##@!#!^%&^$*"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdAASDSADSAD##@!#!^%&^$*aa"));
-        judgeValues.add(PasswordUtility.judgePasswordStrength("3sadasdAASDSADSAD##@!#!^%&^$*aaaaaaaaaaaa"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,""));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasd"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdA"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdAASDSADSAD"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdAASDSADSAD#"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdAASDSADSAD##@!#!^%&^$*"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdAASDSADSAD##@!#!^%&^$*aa"));
+        judgeValues.add(PasswordUtility.judgePasswordStrength(configuration,"3sadasdAASDSADSAD##@!#!^%&^$*aaaaaaaaaaaa"));
         /*
         judgeValues.add(0);
         judgeValues.add(1);