Selaa lähdekoodia

Merge branch 'master' of github.com:pwm-project/pwm into ux-changes

Joseph White 8 vuotta sitten
vanhempi
commit
b91d0a8bc6
100 muutettua tiedostoa jossa 1770 lisäystä ja 595 poistoa
  1. 6 0
      src/main/java/password/pwm/AppProperty.java
  2. 6 1
      src/main/java/password/pwm/PwmApplication.java
  3. 1 0
      src/main/java/password/pwm/bean/SessionLabel.java
  4. 30 4
      src/main/java/password/pwm/config/Configuration.java
  5. 6 0
      src/main/java/password/pwm/config/FormConfiguration.java
  6. 1 1
      src/main/java/password/pwm/config/FormUtility.java
  7. 37 0
      src/main/java/password/pwm/config/NamedSecretData.java
  8. 1 4
      src/main/java/password/pwm/config/PwmSetting.java
  9. 2 0
      src/main/java/password/pwm/config/PwmSettingSyntax.java
  10. 27 0
      src/main/java/password/pwm/config/option/WebServiceUsage.java
  11. 3 3
      src/main/java/password/pwm/config/profile/NewUserProfile.java
  12. 4 0
      src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  13. 197 0
      src/main/java/password/pwm/config/value/NamedSecretValue.java
  14. 86 19
      src/main/java/password/pwm/http/IdleTimeoutCalculator.java
  15. 1 0
      src/main/java/password/pwm/http/PwmRequestAttribute.java
  16. 17 3
      src/main/java/password/pwm/http/PwmSessionWrapper.java
  17. 17 37
      src/main/java/password/pwm/http/bean/NewUserBean.java
  18. 6 26
      src/main/java/password/pwm/http/bean/PwmSessionBean.java
  19. 4 7
      src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  20. 71 0
      src/main/java/password/pwm/http/servlet/newuser/NewUserForm.java
  21. 66 94
      src/main/java/password/pwm/http/servlet/newuser/NewUserFormUtils.java
  22. 37 18
      src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  23. 14 7
      src/main/java/password/pwm/http/servlet/newuser/NewUserTokenData.java
  24. 20 9
      src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  25. 2 2
      src/main/java/password/pwm/http/state/SessionStateService.java
  26. 4 3
      src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  27. 4 1
      src/main/java/password/pwm/svc/PwmServiceManager.java
  28. 35 0
      src/main/java/password/pwm/svc/cluster/ClusterProvider.java
  29. 106 0
      src/main/java/password/pwm/svc/cluster/ClusterService.java
  30. 240 0
      src/main/java/password/pwm/svc/cluster/DatabaseClusterProvider.java
  31. 50 0
      src/main/java/password/pwm/svc/cluster/DatabaseClusterSettings.java
  32. 54 0
      src/main/java/password/pwm/svc/cluster/DatabaseStoredNodeData.java
  33. 18 13
      src/main/java/password/pwm/svc/cluster/NodeInfo.java
  34. 4 13
      src/main/java/password/pwm/svc/event/AuditService.java
  35. 1 1
      src/main/java/password/pwm/svc/event/SyslogAuditService.java
  36. 6 6
      src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java
  37. 9 8
      src/main/java/password/pwm/svc/wordlist/Populator.java
  38. 7 78
      src/main/java/password/pwm/svc/wordlist/StoredWordlistDataBean.java
  39. 10 2
      src/main/java/password/pwm/util/BasicAuthInfo.java
  40. 97 102
      src/main/java/password/pwm/util/RandomPasswordGenerator.java
  41. 2 2
      src/main/java/password/pwm/util/cli/MainClass.java
  42. 19 10
      src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java
  43. 1 0
      src/main/java/password/pwm/util/db/DatabaseTable.java
  44. 2 1
      src/main/java/password/pwm/util/java/JavaHelper.java
  45. 3 3
      src/main/java/password/pwm/util/java/TimeDuration.java
  46. 3 2
      src/main/java/password/pwm/util/localdb/Derby_LocalDB.java
  47. 8 40
      src/main/java/password/pwm/ws/server/RestRequestBean.java
  48. 1 1
      src/main/java/password/pwm/ws/server/RestResultBean.java
  49. 7 20
      src/main/java/password/pwm/ws/server/RestServerHelper.java
  50. 89 0
      src/main/java/password/pwm/ws/server/StandaloneRestHelper.java
  51. 37 0
      src/main/java/password/pwm/ws/server/StandaloneRestRequestBean.java
  52. 1 1
      src/main/java/password/pwm/ws/server/rest/RestAppDataServer.java
  53. 1 1
      src/main/java/password/pwm/ws/server/rest/RestChallengesServer.java
  54. 10 8
      src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  55. 15 14
      src/main/java/password/pwm/ws/server/rest/RestSigningServer.java
  56. 1 1
      src/main/java/password/pwm/ws/server/rest/RestStatusServer.java
  57. 1 1
      src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java
  58. 1 1
      src/main/java/password/pwm/ws/server/rest/RestVerifyResponsesServer.java
  59. 7 1
      src/main/resources/password/pwm/AppProperty.properties
  60. 56 9
      src/main/resources/password/pwm/config/PwmSetting.xml
  61. 1 1
      src/main/resources/password/pwm/i18n/Admin.properties
  62. 12 1
      src/main/resources/password/pwm/i18n/Display_ca.properties
  63. 12 1
      src/main/resources/password/pwm/i18n/Display_da.properties
  64. 12 1
      src/main/resources/password/pwm/i18n/Display_de.properties
  65. 12 1
      src/main/resources/password/pwm/i18n/Display_es.properties
  66. 12 1
      src/main/resources/password/pwm/i18n/Display_fr.properties
  67. 12 1
      src/main/resources/password/pwm/i18n/Display_it.properties
  68. 12 1
      src/main/resources/password/pwm/i18n/Display_ja.properties
  69. 12 1
      src/main/resources/password/pwm/i18n/Display_nl.properties
  70. 12 1
      src/main/resources/password/pwm/i18n/Display_pl.properties
  71. 12 1
      src/main/resources/password/pwm/i18n/Display_pt_BR.properties
  72. 2 0
      src/main/resources/password/pwm/i18n/Display_ru.properties
  73. 12 1
      src/main/resources/password/pwm/i18n/Display_sv.properties
  74. 12 1
      src/main/resources/password/pwm/i18n/Display_zh_CN.properties
  75. 12 1
      src/main/resources/password/pwm/i18n/Display_zh_TW.properties
  76. 1 0
      src/main/resources/password/pwm/i18n/Error_ca.properties
  77. 1 0
      src/main/resources/password/pwm/i18n/Error_da.properties
  78. 1 0
      src/main/resources/password/pwm/i18n/Error_de.properties
  79. 1 0
      src/main/resources/password/pwm/i18n/Error_es.properties
  80. 1 0
      src/main/resources/password/pwm/i18n/Error_fr.properties
  81. 1 0
      src/main/resources/password/pwm/i18n/Error_it.properties
  82. 1 0
      src/main/resources/password/pwm/i18n/Error_ja.properties
  83. 1 0
      src/main/resources/password/pwm/i18n/Error_nl.properties
  84. 1 0
      src/main/resources/password/pwm/i18n/Error_pl.properties
  85. 1 0
      src/main/resources/password/pwm/i18n/Error_pt_BR.properties
  86. 1 0
      src/main/resources/password/pwm/i18n/Error_ru.properties
  87. 1 0
      src/main/resources/password/pwm/i18n/Error_sv.properties
  88. 1 0
      src/main/resources/password/pwm/i18n/Error_zh_CN.properties
  89. 1 0
      src/main/resources/password/pwm/i18n/Error_zh_TW.properties
  90. 2 0
      src/main/resources/password/pwm/i18n/Message_ca.properties
  91. 2 0
      src/main/resources/password/pwm/i18n/Message_da.properties
  92. 2 0
      src/main/resources/password/pwm/i18n/Message_de.properties
  93. 2 0
      src/main/resources/password/pwm/i18n/Message_es.properties
  94. 3 1
      src/main/resources/password/pwm/i18n/Message_fr.properties
  95. 2 0
      src/main/resources/password/pwm/i18n/Message_it.properties
  96. 2 0
      src/main/resources/password/pwm/i18n/Message_ja.properties
  97. 3 1
      src/main/resources/password/pwm/i18n/Message_nl.properties
  98. 2 0
      src/main/resources/password/pwm/i18n/Message_pl.properties
  99. 3 1
      src/main/resources/password/pwm/i18n/Message_pt_BR.properties
  100. 2 0
      src/main/resources/password/pwm/i18n/Message_ru.properties

+ 6 - 0
src/main/java/password/pwm/AppProperty.java

@@ -69,6 +69,10 @@ public enum     AppProperty {
     CONFIG_GUIDE_IDLE_TIMEOUT                       ("configGuide.idleTimeoutSeconds"),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES             ("configManager.zipDebug.maxLogLines"),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ("configManager.zipDebug.maxLogSeconds"),
+    CLUSTER_DB_ENABLE                               ("cluster.db.enable"),
+    CLUSTER_DB_HEARTBEAT_SECONDS                    ("cluster.db.heartbeatSeconds"),
+    CLUSTER_DB_NODE_TIMEOUT_SECONDS                 ("cluster.db.nodeTimeoutSeconds"),
+    CLUSTER_DB_NODE_PURGE_SECONDS                   ("cluster.db.nodePurgeSeconds"),
     DB_JDBC_LOAD_STRATEGY                           ("db.jdbcLoadStrategy"),
     DB_CONNECTIONS_MAX                              ("db.connections.max"),
     DB_CONNECTIONS_TIMEOUT_MS                       ("db.connections.timeoutMs"),
@@ -141,6 +145,7 @@ public enum     AppProperty {
     LOCALDB_AGGRESSIVE_COMPACT_ENABLED              ("localdb.aggressiveCompact.enabled"),
     LOCALDB_IMPLEMENTATION                          ("localdb.implementation"),
     LOCALDB_INIT_STRING                             ("localdb.initParameters"),
+    LOCALDB_LOCATION                                ("localdb.location"),
     LOCALDB_LOGWRITER_BUFFER_SIZE                   ("localdb.logWriter.bufferSize"),
     LOCALDB_LOGWRITER_MAX_BUFFER_WAIT_MS            ("localdb.logWriter.maxBufferWaitMs"),
     LOCALDB_LOGWRITER_MAX_TRIM_SIZE                 ("localdb.logWriter.maxTrimSize"),
@@ -290,6 +295,7 @@ public enum     AppProperty {
     WORDLIST_CHAR_LENGTH_MAX                        ("wordlist.maxCharLength"),
     WORDLIST_CHAR_LENGTH_MIN                        ("wordlist.minCharLength"),
     WS_REST_CLIENT_PWRULE_HALTONERROR               ("ws.restClient.pwRule.haltOnError"),
+    WS_REST_SERVER_SIGNING_FORM_TIMEOUT_SECONDS     ("ws.restServer.signing.form.timeoutSeconds"),
     ALLOW_MACRO_IN_REGEX_SETTING                    ("password.policy.allowMacroInRegexSetting"),
 
     ;

+ 6 - 1
src/main/java/password/pwm/PwmApplication.java

@@ -42,6 +42,7 @@ import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.cache.CacheService;
+import password.pwm.svc.cluster.ClusterService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditService;
@@ -503,6 +504,10 @@ public class PwmApplication {
         return (VersionChecker)pwmServiceManager.getService(VersionChecker.class);
     }
 
+    public ClusterService getClusterService() {
+        return (ClusterService) pwmServiceManager.getService(ClusterService.class);
+    }
+
     public ErrorInformation getLastLocalDBFailure() {
         return lastLocalDBFailure;
     }
@@ -697,7 +702,7 @@ public class PwmApplication {
             final File databaseDirectory;
             // see if META-INF isn't already there, then use WEB-INF.
             try {
-                final String localDBLocationSetting = pwmApplication.getConfig().readSettingAsString(PwmSetting.PWMDB_LOCATION);
+                final String localDBLocationSetting = pwmApplication.getConfig().readAppProperty(AppProperty.LOCALDB_LOCATION);
                 databaseDirectory = FileSystemUtility.figureFilepath(localDBLocationSetting, pwmApplication.pwmEnvironment.getApplicationPath());
             } catch (Exception e) {
                 pwmApplication.lastLocalDBFailure = new ErrorInformation(PwmError.ERROR_LOCALDB_UNAVAILABLE,"error locating configured LocalDB directory: " + e.getMessage());

+ 1 - 0
src/main/java/password/pwm/bean/SessionLabel.java

@@ -37,6 +37,7 @@ public class SessionLabel implements Serializable {
     public static final SessionLabel CLI_SESSION_LABEL= new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"cli",null,null);
     public static final SessionLabel HEALTH_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"health",null,null);
     public static final SessionLabel REPORTING_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"reporting",null,null);
+    public static final SessionLabel AUDITING_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"auditing",null,null);
 
     private final String sessionID;
     private final UserIdentity userIdentity;

+ 30 - 4
src/main/java/password/pwm/config/Configuration.java

@@ -51,6 +51,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StringArrayValue;
@@ -96,6 +97,9 @@ public class Configuration implements Serializable, SettingReader {
 
     private DataCache dataCache = new DataCache();
 
+    private String cachshedConfigurationHash;
+
+
     // --------------------------- CONSTRUCTORS ---------------------------
 
     public Configuration(final StoredConfigurationImpl storedConfiguration) {
@@ -187,6 +191,11 @@ public class Configuration implements Serializable, SettingReader {
         return JavaTypeConverter.valueToPassword(readStoredValue(setting));
     }
 
+    public Map<String,NamedSecretData> readSettingAsNamedPasswords(final PwmSetting setting)
+    {
+        return JavaTypeConverter.valueToNamedPassword(readStoredValue(setting));
+    }
+
     public abstract static class JavaTypeConverter {
         public static long valueToLong(final StoredValue value) {
             if (!(value instanceof NumericValue)) {
@@ -222,7 +231,21 @@ public class Configuration implements Serializable, SettingReader {
             }
             return (PasswordData)nativeObject;
         }
-        
+
+        public static Map<String,NamedSecretData> valueToNamedPassword(final StoredValue value) {
+            if (value == null) {
+                return null;
+            }
+            if ((!(value instanceof NamedSecretValue))) {
+                throw new IllegalArgumentException("setting value is not readable as named password");
+            }
+            final Object nativeObject = value.toNativeObject();
+            if (nativeObject == null) {
+                return null;
+            }
+            return (Map<String,NamedSecretData>)nativeObject;
+        }
+
         public static List<ActionConfiguration> valueToAction(final PwmSetting setting, final StoredValue storedValue) {
             if (PwmSettingSyntax.ACTION != setting.getSyntax()) {
                 throw new IllegalArgumentException("may not read ACTION value for setting: " + setting.toString());
@@ -882,11 +905,14 @@ public class Configuration implements Serializable, SettingReader {
     public boolean isDevDebugMode() {
         return Boolean.parseBoolean(readAppProperty(AppProperty.LOGGING_DEV_OUTPUT));
     }
-    
-    public String configurationHash() 
+
+    public String configurationHash()
             throws PwmUnrecoverableException 
     {
-        return storedConfiguration.settingChecksum();
+        if (this.cachshedConfigurationHash == null) {
+            this.cachshedConfigurationHash = storedConfiguration.settingChecksum();
+        }
+        return cachshedConfigurationHash;
     }
 
     public Set<PwmSetting> nonDefaultSettings() {

+ 6 - 0
src/main/java/password/pwm/config/FormConfiguration.java

@@ -266,6 +266,12 @@ public class FormConfiguration implements Serializable {
 
     public void checkValue(final Configuration config, final String value, final Locale locale)
             throws PwmDataValidationException, PwmUnrecoverableException {
+
+        // ignore read only fields
+        if (readonly) {
+            return;
+        }
+
         //check if value is missing and required.
         if (required && (value == null || value.length() < 1)) {
             final ErrorInformation error = new ErrorInformation(PwmError.ERROR_FIELD_REQUIRED, null, new String[]{getLabel(locale)});

+ 1 - 1
src/main/java/password/pwm/config/FormUtility.java

@@ -90,7 +90,7 @@ public class FormUtility {
             final String keyName = formItem.getName();
             final String value = inputMap.get(keyName);
 
-            if (formItem.isRequired()) {
+            if (formItem.isRequired() && !formItem.isReadonly()) {
                 if (value == null || value.length() < 0) {
                     final String errorMsg = "missing required value for field '" + formItem.getName() + "'";
                     final ErrorInformation error = new ErrorInformation(PwmError.ERROR_FIELD_REQUIRED, errorMsg, new String[]{formItem.getLabel(locale)});

+ 37 - 0
src/main/java/password/pwm/config/NamedSecretData.java

@@ -0,0 +1,37 @@
+/*
+ * 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
+ */
+
+package password.pwm.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.util.PasswordData;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@AllArgsConstructor
+public class NamedSecretData implements Serializable {
+    private PasswordData password;
+    private List<String> usage;
+}

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

@@ -1098,7 +1098,7 @@ public enum PwmSetting {
     WEBSERVICES_THIRDPARTY_QUERY_MATCH(
             "webservices.thirdParty.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REST_SERVER),
     WEBSERVICES_EXTERNAL_SECRET(
-            "webservices.external.secret", PwmSettingSyntax.PASSWORD, PwmSettingCategory.REST_SERVER),
+            "webservices.external.secrets", PwmSettingSyntax.NAMED_SECRET, PwmSettingCategory.REST_SERVER),
 
 
     EXTERNAL_MACROS_DEST_TOKEN_URLS(
@@ -1131,9 +1131,6 @@ public enum PwmSetting {
             "recovery.require.otp", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS),
     HELPDESK_ENABLE_OTP_VERIFY(
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE),
-    PWMDB_LOCATION(
-            "pwmDb.location", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL),
-
 
 
     ;

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

@@ -30,6 +30,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.PasswordValue;
@@ -68,6 +69,7 @@ public enum PwmSettingSyntax {
     PROFILE(StringArrayValue.factory()),
     VERIFICATION_METHOD(VerificationMethodValue.factory()),
     PRIVATE_KEY(PrivateKeyValue.factory()),
+    NAMED_SECRET(NamedSecretValue.factory()),
 
     ;
 

+ 27 - 0
src/main/java/password/pwm/config/option/WebServiceUsage.java

@@ -0,0 +1,27 @@
+/*
+ * 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
+ */
+
+package password.pwm.config.option;
+
+public enum WebServiceUsage {
+    SigningForm
+}

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

@@ -39,7 +39,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.operations.PasswordUtility;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -48,7 +48,7 @@ public class NewUserProfile extends AbstractProfile {
 
     private static final ProfileType PROFILE_TYPE = ProfileType.NewUser;
 
-    private Date newUserPasswordPolicyCacheTime;
+    private Instant newUserPasswordPolicyCacheTime;
     private final Map<Locale,PwmPasswordPolicy> newUserPasswordPolicyCache = new HashMap<>();
 
     protected NewUserProfile(final String identifier, final Map<PwmSetting, StoredValue> storedValueMap) {
@@ -78,7 +78,7 @@ public class NewUserProfile extends AbstractProfile {
         final Configuration config = pwmApplication.getConfig();
         final long maxNewUserCacheMS = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_NEWUSER_PASSWORD_POLICY_CACHE_MS));
         if (newUserPasswordPolicyCacheTime != null && TimeDuration.fromCurrent(newUserPasswordPolicyCacheTime).isLongerThan(maxNewUserCacheMS)) {
-            newUserPasswordPolicyCacheTime = new Date();
+            newUserPasswordPolicyCacheTime = Instant.now();
             newUserPasswordPolicyCache.clear();
         }
 

+ 4 - 0
src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -41,6 +41,7 @@ import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.PrivateKeyValue;
 import password.pwm.config.value.StringArrayValue;
@@ -810,6 +811,9 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
             } else if (setting.getSyntax() == PwmSettingSyntax.PRIVATE_KEY) {
                 final List<Element> valueElements = ((PrivateKeyValue)value).toXmlValues("value", getKey());
                 settingElement.addContent(valueElements);
+            } else if (setting.getSyntax() == PwmSettingSyntax.NAMED_SECRET) {
+                final List<Element> valueElements = ((NamedSecretValue)value).toXmlValues("value", getKey());
+                settingElement.addContent(valueElements);
             } else {
                 settingElement.addContent(value.toXmlValues("value"));
             }

+ 197 - 0
src/main/java/password/pwm/config/value/NamedSecretValue.java

@@ -0,0 +1,197 @@
+/*
+ * 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
+ */
+
+package password.pwm.config.value;
+
+import com.google.gson.reflect.TypeToken;
+import org.jdom2.Element;
+import password.pwm.PwmConstants;
+import password.pwm.config.NamedSecretData;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.secure.PwmBlockAlgorithm;
+import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class NamedSecretValue implements StoredValue {
+    private static final String ELEMENT_NAME = "name";
+    private static final String ELEMENT_PASSWORD = "password";
+    private static final String ELEMENT_USAGE = "usage";
+
+    private Map<String,NamedSecretData> values;
+
+    NamedSecretValue() {
+    }
+
+
+    public NamedSecretValue(final Map<String,NamedSecretData> values) {
+        this.values = values;
+    }
+
+    public static StoredValue.StoredValueFactory factory()
+    {
+        return new StoredValue.StoredValueFactory() {
+            public NamedSecretValue fromJson(final String value)
+            {
+                try {
+                    final Map<String,NamedSecretData> values = JsonUtil.deserialize(value,new TypeToken<Map<String,NamedSecretData>>() {
+                    }.getType());
+                    final Map<String,NamedSecretData> linkedValues = new LinkedHashMap<>(values);
+                    return new NamedSecretValue(linkedValues);
+                } catch (Exception e) {
+                    throw new IllegalStateException(
+                            "NamedPasswordValue can not be json de-serialized: " + e.getMessage());
+                }
+            }
+
+            public NamedSecretValue fromXmlElement(
+                    final Element settingElement,
+                    final PwmSecurityKey key
+            )
+                    throws PwmOperationalException, PwmUnrecoverableException
+            {
+                final Map<String,NamedSecretData> values = new LinkedHashMap<>();
+                final List<Element> valueElements = settingElement.getChildren("value");
+
+                try {
+                    if (valueElements != null) {
+                        for (final Element value : valueElements) {
+                            if (value.getChild(ELEMENT_NAME) != null && value.getChild(ELEMENT_PASSWORD) != null) {
+                                final String name = value.getChild(ELEMENT_NAME).getText();
+                                final String encodedValue = value.getChild(ELEMENT_PASSWORD).getText();
+                                final PasswordData passwordData = new PasswordData(SecureEngine.decryptStringValue(encodedValue, key, PwmBlockAlgorithm.CONFIG));
+                                final List<Element> usages = value.getChildren(ELEMENT_USAGE);
+                                final List<String> strUsages = new ArrayList<>();
+                                if (usages != null) {
+                                    for (final Element usageElement : usages) {
+                                        strUsages.add(usageElement.getText());
+                                    }
+                                }
+                                values.put(name, new NamedSecretData(passwordData, Collections.unmodifiableList(strUsages)));
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, errorMsg);
+                    throw new PwmOperationalException(errorInfo);
+                }
+                return new NamedSecretValue(values);
+            }
+        };
+    }
+
+    public List<Element> toXmlValues(final String valueElementName) {
+        throw new IllegalStateException("password xml output requires hash key");
+    }
+
+    @Override
+    public Object toNativeObject()
+    {
+        return values;
+    }
+
+    @Override
+    public List<String> validateValue(final PwmSetting pwm)
+    {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public int currentSyntaxVersion()
+    {
+        return 0;
+    }
+
+    public List<Element> toXmlValues(final String valueElementName, final PwmSecurityKey key) {
+        if (values == null) {
+            final Element valueElement = new Element(valueElementName);
+            return Collections.singletonList(valueElement);
+        }
+        final List<Element> valuesElement = new ArrayList<>();
+        try {
+            for (final String name : values.keySet()) {
+                final PasswordData passwordData = values.get(name).getPassword();
+                final String encodedValue = SecureEngine.encryptToString(passwordData.getStringValue(), key, PwmBlockAlgorithm.CONFIG);
+                final Element newValueElement = new Element("value");
+                final Element nameElement = new Element(ELEMENT_NAME);
+                nameElement.setText(name);
+                final Element encodedValueElement = new Element(ELEMENT_PASSWORD);
+                encodedValueElement.setText(encodedValue);
+
+                newValueElement.addContent(nameElement);
+                newValueElement.addContent(encodedValueElement);
+
+                for (final String usages : values.get(name).getUsage()) {
+                    final Element usageElement = new Element(ELEMENT_USAGE);
+                    usageElement.setText(usages);
+                    newValueElement.addContent(usageElement);
+                }
+
+
+                valuesElement.add(newValueElement);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage());
+        }
+        return Collections.unmodifiableList(valuesElement);
+    }
+
+    public String toString() {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    @Override
+    public String toDebugString(final Locale locale) {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    @Override
+    public Serializable toDebugJsonObject(final Locale locale) {
+        return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+    }
+
+    public boolean requiresStoredUpdate()
+    {
+        return false;
+    }
+
+    @Override
+    public String valueHash() throws PwmUnrecoverableException {
+        return values == null ? "" : SecureEngine.hash(JsonUtil.serializeMap(values), PwmConstants.SETTING_CHECKSUM_HASH_METHOD);
+    }
+
+}

+ 86 - 19
src/main/java/password/pwm/http/IdleTimeoutCalculator.java

@@ -22,65 +22,132 @@
 
 package password.pwm.http;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
+import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.HelpdeskProfile;
+import password.pwm.config.profile.ProfileType;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.ldap.UserInfo;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
+import java.util.Collections;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 
 public class IdleTimeoutCalculator {
     private static final PwmLogger LOGGER = PwmLogger.forClass(IdleTimeoutCalculator.class);
 
-    public static TimeDuration figureMaxIdleTimeout(final PwmApplication pwmApplication, final PwmSession pwmSession) throws PwmUnrecoverableException
+    public static MaxIdleTimeoutResult figureMaxSessionTimeout(final PwmApplication pwmApplication, final PwmSession pwmSession)
+            throws PwmUnrecoverableException
     {
         final Configuration configuration = pwmApplication.getConfig();
-        long idleSeconds = configuration.readSettingAsLong(PwmSetting.IDLE_TIMEOUT_SECONDS);
+        final SortedSet<MaxIdleTimeoutResult> results = new TreeSet<>();
+        {
+            final long idleSetting = configuration.readSettingAsLong(PwmSetting.IDLE_TIMEOUT_SECONDS);
+            results.add(new MaxIdleTimeoutResult(
+                    MaxIdleTimeoutResult.reasonFor(PwmSetting.IDLE_TIMEOUT_SECONDS, null),
+                    new TimeDuration(idleSetting, TimeUnit.SECONDS)));
+        }
 
         if (!pwmSession.isAuthenticated()) {
+            if (pwmApplication.getApplicationMode() == PwmApplicationMode.NEW) {
+                final long configGuideIdleTimeout = Long.parseLong(configuration.readAppProperty(AppProperty.CONFIG_GUIDE_IDLE_TIMEOUT));
+                results.add(new MaxIdleTimeoutResult(
+                        "Configuration Guide Idle Timeout",
+                        new TimeDuration(configGuideIdleTimeout, TimeUnit.SECONDS)));
+            }
+
             if (configuration.readSettingAsBoolean(PwmSetting.PEOPLE_SEARCH_ENABLE_PUBLIC)) {
                 final long peopleSearchIdleTimeout = configuration.readSettingAsLong(PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS);
-                idleSeconds = Math.max(idleSeconds, peopleSearchIdleTimeout);
+                if (peopleSearchIdleTimeout > 0) {
+                    results.add(new MaxIdleTimeoutResult(
+                            MaxIdleTimeoutResult.reasonFor(PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS, null),
+                            new TimeDuration(peopleSearchIdleTimeout, TimeUnit.SECONDS)));
+                }
             }
 
-            return TimeDuration.fromCurrent(idleSeconds);
+        } else {
+            final UserInfo userInfo= pwmSession.getUserInfo();
+            final boolean userIsAdmin = pwmSession.getSessionManager().checkPermission(pwmApplication, Permission.PWMADMIN);
+            final Set<MaxIdleTimeoutResult> loggedInResults = figureMaxAuthUserTimeout(configuration, userInfo, userIsAdmin);
+            results.addAll(loggedInResults);
+        }
+
+        return results.last();
+    }
+
+    private static Set<MaxIdleTimeoutResult> figureMaxAuthUserTimeout(
+            final Configuration configuration,
+            final UserInfo userInfo,
+            final boolean userIsAdmin
+    )
+            throws PwmUnrecoverableException
+    {
+        final Set<MaxIdleTimeoutResult> results = new TreeSet<>();
+        {
+            final long idleSetting = configuration.readSettingAsLong(PwmSetting.IDLE_TIMEOUT_SECONDS);
+            results.add(new MaxIdleTimeoutResult(
+                    MaxIdleTimeoutResult.reasonFor(PwmSetting.IDLE_TIMEOUT_SECONDS, null),
+                    new TimeDuration(idleSetting, TimeUnit.SECONDS)));
         }
 
         if (configuration.readSettingAsBoolean(PwmSetting.HELPDESK_ENABLE)) {
-            final HelpdeskProfile helpdeskProfile = pwmSession.getSessionManager().getHelpdeskProfile(pwmApplication);
-            if (helpdeskProfile != null) {
+            final String helpdeskProfileID = userInfo.getProfileIDs().get(ProfileType.Helpdesk);
+            if (!StringUtil.isEmpty(helpdeskProfileID)) {
+                final HelpdeskProfile helpdeskProfile = configuration.getHelpdeskProfiles().get(helpdeskProfileID);
                 final long helpdeskIdleTimeout = helpdeskProfile.readSettingAsLong(PwmSetting.HELPDESK_IDLE_TIMEOUT_SECONDS);
-                idleSeconds = Math.max(idleSeconds, helpdeskIdleTimeout);
+                results.add(new MaxIdleTimeoutResult(
+                        MaxIdleTimeoutResult.reasonFor(PwmSetting.HELPDESK_IDLE_TIMEOUT_SECONDS, helpdeskProfileID),
+                        new TimeDuration(helpdeskIdleTimeout, TimeUnit.SECONDS)));
             }
         }
 
         if (configuration.readSettingAsBoolean(PwmSetting.PEOPLE_SEARCH_ENABLE)) {
             final long peopleSearchIdleTimeout = configuration.readSettingAsLong(PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS);
-            idleSeconds = Math.max(idleSeconds, peopleSearchIdleTimeout);
+            if (peopleSearchIdleTimeout > 0) {
+                results.add(new MaxIdleTimeoutResult(
+                        MaxIdleTimeoutResult.reasonFor(PwmSetting.PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS, null),
+                        new TimeDuration(peopleSearchIdleTimeout, TimeUnit.SECONDS)));
+            }
         }
 
-        if (pwmApplication.getApplicationMode() == PwmApplicationMode.NEW) {
-            final long configGuideIdleTimeout = Long.parseLong(configuration.readAppProperty(AppProperty.CONFIG_GUIDE_IDLE_TIMEOUT));
-            idleSeconds = Math.max(idleSeconds, configGuideIdleTimeout);
+        if (userIsAdmin) {
+            final long configEditorIdleTimeout = Long.parseLong(configuration.readAppProperty(AppProperty.CONFIG_EDITOR_IDLE_TIMEOUT));
+            results.add(new MaxIdleTimeoutResult(
+                    "Config Editor Idle Timeout",
+                    new TimeDuration(configEditorIdleTimeout, TimeUnit.SECONDS)));
         }
 
-        try {
-            if (pwmSession.getSessionManager().checkPermission(pwmApplication, Permission.PWMADMIN)) {
-                final long configEditorIdleTimeout = Long.parseLong(configuration.readAppProperty(AppProperty.CONFIG_EDITOR_IDLE_TIMEOUT));
-                idleSeconds = Math.max(idleSeconds, configEditorIdleTimeout);
-            }
-        } catch (PwmUnrecoverableException e) {
-            LOGGER.error(pwmSession,"error while figuring max idle timeout for session: " + e.getMessage());
+        return Collections.unmodifiableSet(results);
+    }
+
+    @Getter
+    @AllArgsConstructor
+    static class MaxIdleTimeoutResult implements Comparable<MaxIdleTimeoutResult> {
+        private final String reason;
+        private final TimeDuration idleTimeout;
+
+        @Override
+        public int compareTo(final MaxIdleTimeoutResult o)
+        {
+            return this.idleTimeout.compareTo(o.getIdleTimeout());
         }
 
-        return new TimeDuration(idleSeconds, TimeUnit.SECONDS);
+        static String reasonFor(final PwmSetting pwmSetting, final String profileID) {
+            return "Setting " + pwmSetting.toMenuLocationDebug(profileID, PwmConstants.DEFAULT_LOCALE);
+        }
     }
 
     public static TimeDuration idleTimeoutForRequest(final PwmRequest pwmRequest) throws PwmUnrecoverableException

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

@@ -38,6 +38,7 @@ public enum PwmRequestAttribute {
     CspNonce,
 
     FormConfiguration,
+    FormInitialValues,
     FormReadOnly,
     FormShowPasswordFields,
     FormData,

+ 17 - 3
src/main/java/password/pwm/http/PwmSessionWrapper.java

@@ -27,12 +27,13 @@ import password.pwm.PwmConstants;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
 public class PwmSessionWrapper {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(PwmSessionWrapper.class);
 
     private transient PwmSession pwmSession;
 
@@ -49,8 +50,7 @@ public class PwmSessionWrapper {
     {
         httpSession.setAttribute(PwmConstants.SESSION_ATTR_PWM_SESSION, pwmSession);
 
-        final TimeDuration maxIdleTime = IdleTimeoutCalculator.figureMaxIdleTimeout(pwmApplication, pwmSession);
-        httpSession.setMaxInactiveInterval((int) maxIdleTime.getTotalSeconds());
+        setHttpSessionIdleTimeout(pwmApplication, pwmSession, httpSession);
     }
 
 
@@ -67,4 +67,18 @@ public class PwmSessionWrapper {
     public static PwmSession readPwmSession(final HttpServletRequest httpRequest) throws PwmUnrecoverableException {
         return readPwmSession(httpRequest.getSession());
     }
+
+    public static void setHttpSessionIdleTimeout(
+            final PwmApplication pwmApplication,
+            final PwmSession pwmSession,
+            final HttpSession httpSession
+    ) throws PwmUnrecoverableException
+    {
+        final IdleTimeoutCalculator.MaxIdleTimeoutResult result = IdleTimeoutCalculator.figureMaxSessionTimeout(pwmApplication, pwmSession);
+        if (httpSession.getMaxInactiveInterval() != result.getIdleTimeout().getTotalSeconds()) {
+            httpSession.setMaxInactiveInterval((int) result.getIdleTimeout().getTotalSeconds());
+            LOGGER.trace(pwmSession, "setting java servlet session timeout to " + result.getIdleTimeout().asCompactString()
+                    + " due to " + result.getReason());
+        }
+    }
 }

+ 17 - 37
src/main/java/password/pwm/http/bean/NewUserBean.java

@@ -22,16 +22,14 @@
 
 package password.pwm.http.bean;
 
-import lombok.AllArgsConstructor;
+import com.google.gson.annotations.SerializedName;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.config.option.SessionBeanMode;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.PasswordData;
+import password.pwm.http.servlet.newuser.NewUserForm;
 
-import java.io.Serializable;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
@@ -43,49 +41,31 @@ import java.util.Set;
 @Setter
 @NoArgsConstructor
 public class NewUserBean extends PwmSessionBean {
+    @SerializedName("p")
     private String profileID;
+
+    @SerializedName("f")
     private NewUserForm newUserForm;
 
+    @SerializedName("r")
     private Map<String,String> remoteInputData;
-    private boolean agreementPassed;
-    private boolean formPassed;
-    private Instant createStartTime;
-    private boolean urlSpecifiedProfile;
-    private final TokenVerificationProgress tokenVerificationProgress = new TokenVerificationProgress();
-
-    @Getter
-    @AllArgsConstructor
-    public static class
-    NewUserForm implements Serializable {
-        private final Map<String,String> formData;
-        private final PasswordData newUserPassword;
-        private final PasswordData confirmPassword;
 
-        public boolean isConsistentWith(final NewUserForm otherForm) throws PwmUnrecoverableException {
-            if (otherForm == null) {
-                return false;
-            }
+    @SerializedName("ap")
+    private boolean agreementPassed;
 
-            if (newUserPassword != null && otherForm.newUserPassword == null || newUserPassword == null && otherForm.newUserPassword != null) {
-                return false;
-            }
+    @SerializedName("fp")
+    private boolean formPassed;
 
-            if (newUserPassword == null || !newUserPassword.getStringValue().equals(otherForm.newUserPassword.getStringValue())) {
-                return false;
-            }
+    @SerializedName("t")
+    private Instant createStartTime;
 
-            for (final String formKey : formData.keySet()) {
-                final String value = formData.get(formKey);
-                final String otherValue = otherForm.formData.get(formKey);
-                if (value != null && !value.equals(otherValue)) {
-                    return false;
-                }
-            }
+    @SerializedName("u")
+    private boolean urlSpecifiedProfile;
 
-            return true;
-        }
-    }
+    @SerializedName("v")
+    private final TokenVerificationProgress tokenVerificationProgress = new TokenVerificationProgress();
 
+    @Override
     public Type getType() {
         return Type.PUBLIC;
     }

+ 6 - 26
src/main/java/password/pwm/http/bean/PwmSessionBean.java

@@ -22,13 +22,17 @@
 
 package password.pwm.http.bean;
 
+import lombok.Getter;
+import lombok.Setter;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.error.ErrorInformation;
 
 import java.io.Serializable;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Set;
 
+@Getter
+@Setter
 public abstract class PwmSessionBean implements Serializable {
     public enum Type {
         PUBLIC,
@@ -36,33 +40,9 @@ public abstract class PwmSessionBean implements Serializable {
     }
 
     private String guid;
-    private Date timestamp;
+    private Instant timestamp;
     private ErrorInformation lastError;
 
-    public String getGuid() {
-        return guid;
-    }
-
-    public void setGuid(final String guid) {
-        this.guid = guid;
-    }
-
-    public Date getTimestamp() {
-        return timestamp;
-    }
-
-    public void setTimestamp(final Date timestamp) {
-        this.timestamp = timestamp;
-    }
-
-    public ErrorInformation getLastError() {
-        return lastError;
-    }
-
-    public void setLastError(final ErrorInformation lastError) {
-        this.lastError = lastError;
-    }
-
     public abstract Type getType();
 
     public abstract Set<SessionBeanMode> supportedModes();

+ 4 - 7
src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -468,11 +468,8 @@ public class RequestInitializationFilter implements Filter {
             initializeLocaleAndTheme(pwmRequest);
         }
 
-        // set idle timeout (may get overridden by module-specific values elsewhere
-        if (!pwmURL.isResourceURL() && !pwmURL.isCommandServletURL() && !pwmURL.isWebServiceURL()){
-            final TimeDuration maxIdleTimeout = IdleTimeoutCalculator.figureMaxIdleTimeout(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession());
-            pwmRequest.getHttpServletRequest().getSession().setMaxInactiveInterval((int) maxIdleTimeout.getTotalSeconds());
-        }
+        // set idle timeout
+        PwmSessionWrapper.setHttpSessionIdleTimeout(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), pwmRequest.getHttpServletRequest().getSession());
     }
 
     private static void initializeLocaleAndTheme(
@@ -665,9 +662,9 @@ public class RequestInitializationFilter implements Filter {
         final TimeDuration maxDurationForRequest = IdleTimeoutCalculator.idleTimeoutForRequest(pwmRequest);
         final TimeDuration currentDuration = TimeDuration.fromCurrent(pwmRequest.getHttpServletRequest().getSession().getLastAccessedTime());
         if (currentDuration.isLongerThan(maxDurationForRequest)) {
-            LOGGER.debug("closing session due to idle time, max for request is " + maxDurationForRequest.asCompactString() + ", session idle time is " + currentDuration.asCompactString());
+            LOGGER.debug("unauthenticated session due to idle time, max for request is " + maxDurationForRequest.asCompactString()
+                    + ", session idle time is " + currentDuration.asCompactString());
             pwmRequest.getPwmSession().unauthenticateUser(pwmRequest);
-            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_USERAUTHENTICATED,"idle timeout exceeded"));
         }
     }
 

+ 71 - 0
src/main/java/password/pwm/http/servlet/newuser/NewUserForm.java

@@ -0,0 +1,71 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.servlet.newuser;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
+
+import java.io.Serializable;
+import java.util.Map;
+
+@Getter
+@AllArgsConstructor
+public class NewUserForm implements Serializable {
+
+    @SerializedName("f")
+    private final Map<String,String> formData;
+
+    @SerializedName("p")
+    private final PasswordData newUserPassword;
+
+    @SerializedName("c")
+    private final PasswordData confirmPassword;
+
+    public boolean isConsistentWith(final NewUserForm otherForm) throws PwmUnrecoverableException
+    {
+        if (otherForm == null) {
+            return false;
+        }
+
+        if (newUserPassword != null && otherForm.newUserPassword == null || newUserPassword == null && otherForm.newUserPassword != null) {
+            return false;
+        }
+
+        if (newUserPassword == null || !newUserPassword.getStringValue().equals(otherForm.newUserPassword.getStringValue())) {
+            return false;
+        }
+
+        for (final String formKey : formData.keySet()) {
+            final String value = formData.get(formKey);
+            final String otherValue = otherForm.formData.get(formKey);
+            if (value != null && !value.equals(otherValue)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

+ 66 - 94
src/main/java/password/pwm/http/servlet/newuser/NewUserFormUtils.java

@@ -22,12 +22,11 @@
 
 package password.pwm.http.servlet.newuser;
 
-import password.pwm.AppProperty;
 import password.pwm.config.FormConfiguration;
 import password.pwm.config.FormUtility;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.NewUserProfile;
-import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -36,11 +35,12 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.bean.NewUserBean;
 import password.pwm.svc.token.TokenPayload;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.SecureService;
 
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -51,48 +51,31 @@ class NewUserFormUtils {
     private static final PwmLogger LOGGER = PwmLogger.forClass(NewUserFormUtils.class);
 
 
-    static NewUserBean.NewUserForm readFromRequest(
+    static NewUserForm readFromRequest(
             final PwmRequest pwmRequest,
             final NewUserBean newUserBean
 
     )
             throws PwmDataValidationException, PwmUnrecoverableException
     {
-        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
-        final boolean promptForPassword = newUserProfile.readSettingAsBoolean(PwmSetting.NEWUSER_PROMPT_FOR_PASSWORD);
 
         final Locale userLocale = pwmRequest.getLocale();
         final List<FormConfiguration> newUserForm = NewUserServlet.getFormDefinition(pwmRequest);
         final Map<FormConfiguration, String> userFormValues = FormUtility.readFormValuesFromRequest(pwmRequest,
                 newUserForm, userLocale);
-        final PasswordData passwordData1;
-        final PasswordData passwordData2;
-        if (promptForPassword) {
-            passwordData1 = pwmRequest.readParameterAsPassword(NewUserServlet.FIELD_PASSWORD1);
-            passwordData2 = pwmRequest.readParameterAsPassword(NewUserServlet.FIELD_PASSWORD2);
-        } else {
-            final PwmPasswordPolicy pwmPasswordPolicy = newUserProfile.getNewUserPasswordPolicy(pwmRequest.getPwmApplication(), pwmRequest.getLocale());
-            final PasswordData password = RandomPasswordGenerator.createRandomPassword(pwmRequest.getSessionLabel(), pwmPasswordPolicy, pwmRequest.getPwmApplication());
-            passwordData1 = password;
-            passwordData2 = password;
-        }
-
-        final Map<String,String> mergedData = new LinkedHashMap<>(FormUtility.asStringMap(userFormValues));
-        if (newUserBean.getRemoteInputData() != null) {
-            mergedData.putAll(newUserBean.getRemoteInputData());
-        }
+        final PasswordData passwordData1 = pwmRequest.readParameterAsPassword(NewUserServlet.FIELD_PASSWORD1);
+        final PasswordData passwordData2 = pwmRequest.readParameterAsPassword(NewUserServlet.FIELD_PASSWORD2);
 
-        return new NewUserBean.NewUserForm(mergedData, passwordData1, passwordData2);
+        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
+        return injectRemoteValuesIntoForm(userFormValues, newUserBean.getRemoteInputData(), newUserProfile, passwordData1, passwordData2);
     }
 
-    static NewUserBean.NewUserForm readFromJsonRequest(
-            final PwmRequest pwmRequest
+    static NewUserForm readFromJsonRequest(
+            final PwmRequest pwmRequest,
+            final NewUserBean newUserBean
     )
             throws IOException, PwmUnrecoverableException, PwmDataValidationException
     {
-        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
-
-        final boolean promptForPassword = newUserProfile.readSettingAsBoolean(PwmSetting.NEWUSER_PROMPT_FOR_PASSWORD);
 
         final Locale userLocale = pwmRequest.getLocale();
         final List<FormConfiguration> newUserForm = NewUserServlet.getFormDefinition(pwmRequest);
@@ -100,24 +83,18 @@ class NewUserFormUtils {
         final Map<FormConfiguration, String> userFormValues = FormUtility.readFormValuesFromMap(jsonBodyMap,
                 newUserForm, userLocale);
 
-        final PasswordData passwordData1;
-        final PasswordData passwordData2;
-        if (promptForPassword) {
-            passwordData1 = jsonBodyMap.containsKey(NewUserServlet.FIELD_PASSWORD1) && !jsonBodyMap.get(
-                    NewUserServlet.FIELD_PASSWORD1).isEmpty()
-                    ? new PasswordData(jsonBodyMap.get(NewUserServlet.FIELD_PASSWORD1))
-                    : null;
-            passwordData2 = jsonBodyMap.containsKey(NewUserServlet.FIELD_PASSWORD2) && !jsonBodyMap.get(
-                    NewUserServlet.FIELD_PASSWORD2).isEmpty()
-                    ? new PasswordData(jsonBodyMap.get(NewUserServlet.FIELD_PASSWORD2))
-                    : null;
-        } else {
-            final PwmPasswordPolicy pwmPasswordPolicy = newUserProfile.getNewUserPasswordPolicy(pwmRequest.getPwmApplication(), pwmRequest.getLocale());
-            final PasswordData password = RandomPasswordGenerator.createRandomPassword(pwmRequest.getSessionLabel(), pwmPasswordPolicy, pwmRequest.getPwmApplication());
-            passwordData1 = password;
-            passwordData2 = password;
-        }
-        return new NewUserBean.NewUserForm(FormUtility.asStringMap(userFormValues), passwordData1, passwordData2);
+        final PasswordData passwordData1 = jsonBodyMap.containsKey(NewUserServlet.FIELD_PASSWORD1) && !jsonBodyMap.get(
+                NewUserServlet.FIELD_PASSWORD1).isEmpty()
+                ? new PasswordData(jsonBodyMap.get(NewUserServlet.FIELD_PASSWORD1))
+                : null;
+
+        final PasswordData passwordData2 = jsonBodyMap.containsKey(NewUserServlet.FIELD_PASSWORD2) && !jsonBodyMap.get(
+                NewUserServlet.FIELD_PASSWORD2).isEmpty()
+                ? new PasswordData(jsonBodyMap.get(NewUserServlet.FIELD_PASSWORD2))
+                : null;
+
+        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
+        return injectRemoteValuesIntoForm(userFormValues, newUserBean.getRemoteInputData(), newUserProfile, passwordData1, passwordData2);
     }
 
     static NewUserTokenData fromTokenPayload(
@@ -126,70 +103,40 @@ class NewUserFormUtils {
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final Locale userLocale = pwmRequest.getLocale();
+        final SecureService secureService = pwmRequest.getPwmApplication().getSecureService();
 
         final Map<String, String> payloadMap = tokenPayload.getData();
 
-        final NewUserProfile newUserProfile;
-        {
-            final String profileID = payloadMap.get(NewUserServlet.TOKEN_PAYLOAD_ATTR);
-            payloadMap.remove(NewUserServlet.TOKEN_PAYLOAD_ATTR);
-            if (profileID == null || profileID.isEmpty()) {
-                // typically missing because issued with code before newuser profile existed, so assume  only profile
-                if (pwmRequest.getConfig().getNewUserProfiles().size() > 1) {
-                    throw new PwmOperationalException(PwmError.ERROR_TOKEN_INCORRECT, "token data missing reference to new user profileID");
-                }
-                newUserProfile = pwmRequest.getConfig().getNewUserProfiles().values().iterator().next();
-            } else {
-                if (!pwmRequest.getConfig().getNewUserProfiles().keySet().contains(profileID)) {
-                    throw new PwmOperationalException(PwmError.ERROR_TOKEN_INCORRECT, "token data references an invalid new user profileID");
-                }
-                newUserProfile = pwmRequest.getConfig().getNewUserProfiles().get(profileID);
-            }
+        if (!payloadMap.containsKey(NewUserServlet.TOKEN_PAYLOAD_ATTR)) {
+            throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT, "token is missing new user form data"));
         }
 
-        final List<FormConfiguration> newUserFormDefinition = newUserProfile.readSettingAsForm(PwmSetting.NEWUSER_FORM);
-        final Map<FormConfiguration, String> userFormValues = FormUtility.readFormValuesFromMap(payloadMap,
-                newUserFormDefinition, userLocale);
-        final PasswordData passwordData;
-        if (payloadMap.containsKey(NewUserServlet.FIELD_PASSWORD1)) {
-            final String passwordInToken = payloadMap.get(NewUserServlet.FIELD_PASSWORD1);
-            String decryptedPassword = passwordInToken;
-            try {
-                decryptedPassword = pwmRequest.getPwmApplication().getSecureService().decryptStringValue(passwordInToken);
-            } catch (PwmUnrecoverableException e) {
-                final boolean allowUnencryptedPassword = Boolean.parseBoolean(pwmRequest.getConfig().readAppProperty(AppProperty.NEWUSER_TOKEN_ALLOW_PLAIN_PW));
-                if (allowUnencryptedPassword && e.getError() == PwmError.ERROR_CRYPT_ERROR) {
-                    LOGGER.warn(pwmRequest, "error decrypting password in tokenPayload, will use raw password value: " + e.getMessage());
-                } else {
-                    throw e;
-                }
-            }
-            passwordData = new PasswordData(decryptedPassword);
-        } else {
-            passwordData = null;
-        }
-        final NewUserBean.NewUserForm newUserForm = new NewUserBean.NewUserForm(FormUtility.asStringMap(userFormValues), passwordData, passwordData);
-        return new NewUserTokenData(newUserProfile.getIdentifier(), newUserForm);
+        final String encryptedTokenData = payloadMap.get(NewUserServlet.TOKEN_PAYLOAD_ATTR);
+
+        return secureService.decryptObject(encryptedTokenData, NewUserTokenData.class);
     }
 
     static Map<String, String> toTokenPayload(
             final PwmRequest pwmRequest,
-            final NewUserBean.NewUserForm newUserForm
+            final NewUserBean newUserBean
     )
             throws PwmUnrecoverableException
     {
-        final Map<String, String> payloadMap = new LinkedHashMap<>();
-        payloadMap.put(NewUserServlet.TOKEN_PAYLOAD_ATTR, pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, NewUserBean.class).getProfileID());
-        payloadMap.putAll(newUserForm.getFormData());
-        final String encryptedPassword = pwmRequest.getPwmApplication().getSecureService().encryptToString(
-                newUserForm.getNewUserPassword().getStringValue()
+
+        final NewUserTokenData newUserTokenData = new NewUserTokenData(
+                newUserBean.getProfileID(),
+                newUserBean.getNewUserForm(),
+                newUserBean.getRemoteInputData()
         );
-        payloadMap.put(NewUserServlet.FIELD_PASSWORD1, encryptedPassword);
+
+        final SecureService secureService = pwmRequest.getPwmApplication().getSecureService();
+        final String encodedTokenData = secureService.encryptObjectToString(newUserTokenData);
+        final Map<String, String> payloadMap = new HashMap<>();
+        payloadMap.put(NewUserServlet.TOKEN_PAYLOAD_ATTR, encodedTokenData);
         return payloadMap;
     }
 
-    static Map<String,String> getLdapDataFromNewUserForm(final NewUserProfile newUserProfile, final NewUserBean.NewUserForm newUserForm) {
+    static Map<String,String> getLdapDataFromNewUserForm(final NewUserProfile newUserProfile, final NewUserForm newUserForm) {
         final Map<String,String> ldapData = new LinkedHashMap<>();
         final List<FormConfiguration> formConfigurations = newUserProfile.readSettingAsForm(PwmSetting.NEWUSER_FORM);
         for (final FormConfiguration formConfiguration : formConfigurations) {
@@ -201,4 +148,29 @@ class NewUserFormUtils {
         }
         return ldapData;
     }
+
+    static NewUserForm injectRemoteValuesIntoForm(
+            final Map<FormConfiguration, String> userFormValues,
+            final Map<String,String> injectedValues,
+            final NewUserProfile newUserProfile,
+            final PasswordData passwordData1,
+            final PasswordData passwordData2
+    ) {
+        final Map<String,String> newFormValues = new HashMap<>();
+        newFormValues.putAll(FormUtility.asStringMap(userFormValues));
+
+        final List<FormConfiguration> formConfigurations = newUserProfile.readSettingAsForm(PwmSetting.NEWUSER_FORM);
+        if (injectedValues != null) {
+            for (final FormConfiguration formConfiguration : formConfigurations) {
+                final String name = formConfiguration.getName();
+                if (formConfiguration.isReadonly()
+                        || !newFormValues.containsKey(name) && injectedValues.containsKey(name))
+                {
+                    newFormValues.put(formConfiguration.getName(), injectedValues.get(formConfiguration.getName()));
+                }
+            }
+        }
+
+        return new NewUserForm(newFormValues, passwordData1, passwordData2);
+    }
 }

+ 37 - 18
src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java

@@ -72,6 +72,7 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -97,7 +98,7 @@ public class NewUserServlet extends ControlledPwmServlet {
 
     static final String FIELD_PASSWORD1 = "password1";
     static final String FIELD_PASSWORD2 = "password2";
-    static final String TOKEN_PAYLOAD_ATTR = "_______profileID";
+    static final String TOKEN_PAYLOAD_ATTR = "p";
 
     public enum NewUserAction implements AbstractPwmServlet.ProcessAction {
         profileChoice(HttpMethod.POST, HttpMethod.POST),
@@ -148,8 +149,8 @@ public class NewUserServlet extends ControlledPwmServlet {
 
         final String signedFormData = pwmRequest.readParameterAsString("signedForm", PwmHttpRequestWrapper.Flag.BypassValidation);
         if (!StringUtil.isEmpty(signedFormData)) {
-            LOGGER.trace("detected signedForm parameter in request, will read and place in bean.");
             final Map<String,String> jsonForm = RestSigningServer.readSignedFormValue(pwmApplication, signedFormData);
+            LOGGER.trace("detected signedForm parameter in request, will read and place in bean; keys=" + JsonUtil.serializeCollection(jsonForm.keySet()));
             newUserBean.setRemoteInputData(jsonForm);
         }
 
@@ -308,7 +309,8 @@ public class NewUserServlet extends ControlledPwmServlet {
         final Locale locale = pwmRequest.getLocale();
 
         try {
-            final NewUserBean.NewUserForm newUserForm = NewUserFormUtils.readFromJsonRequest(pwmRequest);
+            final NewUserBean newUserBean = getNewUserBean(pwmRequest);
+            final NewUserForm newUserForm = NewUserFormUtils.readFromJsonRequest(pwmRequest, newUserBean);
             PasswordUtility.PasswordCheckInfo passwordCheckInfo = verifyForm(pwmRequest, newUserForm, true);
             if (passwordCheckInfo.isPassed() && passwordCheckInfo.getMatch() == PasswordUtility.PasswordCheckInfo.MatchStatus.MATCH) {
                 passwordCheckInfo = new PasswordUtility.PasswordCheckInfo(
@@ -336,7 +338,7 @@ public class NewUserServlet extends ControlledPwmServlet {
 
     static PasswordUtility.PasswordCheckInfo verifyForm(
             final PwmRequest pwmRequest,
-            final NewUserBean.NewUserForm newUserForm,
+            final NewUserForm newUserForm,
             final boolean allowResultCaching
     )
             throws PwmDataValidationException, PwmUnrecoverableException, ChaiUnavailableException
@@ -359,15 +361,22 @@ public class NewUserServlet extends ControlledPwmServlet {
                 .cachedPasswordRuleAttributes(FormUtility.asStringMap(formValueData))
                 .passwordPolicy(newUserProfile.getNewUserPasswordPolicy(pwmApplication, locale))
                 .build();
-        return PasswordUtility.checkEnteredPassword(
-                pwmApplication,
-                locale,
-                null,
-                uiBean,
-                null,
-                newUserForm.getNewUserPassword(),
-                newUserForm.getConfirmPassword()
-        );
+
+        final boolean promptForPassword = newUserProfile.readSettingAsBoolean(PwmSetting.NEWUSER_PROMPT_FOR_PASSWORD);
+
+        if (promptForPassword) {
+            return PasswordUtility.checkEnteredPassword(
+                    pwmApplication,
+                    locale,
+                    null,
+                    uiBean,
+                    null,
+                    newUserForm.getNewUserPassword(),
+                    newUserForm.getConfirmPassword()
+            );
+        }
+
+        return new PasswordUtility.PasswordCheckInfo(null, true, 0, PasswordUtility.PasswordCheckInfo.MatchStatus.MATCH, 0);
     }
 
     @ActionHandler(action = "enterCode")
@@ -391,7 +400,7 @@ public class NewUserServlet extends ControlledPwmServlet {
                 final NewUserBean newUserBean = getNewUserBean(pwmRequest);
                 final NewUserTokenData newUserTokenData = NewUserFormUtils.fromTokenPayload(pwmRequest, tokenPayload);
                 newUserBean.setProfileID(newUserTokenData.getProfileID());
-                final NewUserBean.NewUserForm newUserFormFromToken = newUserTokenData.getFormData();
+                final NewUserForm newUserFormFromToken = newUserTokenData.getFormData();
                 if (password.pwm.svc.token.TokenType.NEWUSER_EMAIL.matchesName(tokenPayload.getName())) {
                     LOGGER.debug(pwmRequest, "email token passed");
 
@@ -402,6 +411,7 @@ public class NewUserServlet extends ControlledPwmServlet {
                         throw e;
                     }
 
+                    newUserBean.setRemoteInputData(newUserTokenData.getInjectionData());
                     newUserBean.setNewUserForm(newUserFormFromToken);
                     newUserBean.setFormPassed(true);
                     newUserBean.getTokenVerificationProgress().getPassedTokens().add(TokenVerificationProgress.TokenChannel.EMAIL);
@@ -479,7 +489,7 @@ public class NewUserServlet extends ControlledPwmServlet {
         newUserBean.setNewUserForm(null);
 
         try {
-            final NewUserBean.NewUserForm newUserForm = NewUserFormUtils.readFromRequest(pwmRequest, newUserBean);
+            final NewUserForm newUserForm = NewUserFormUtils.readFromRequest(pwmRequest, newUserBean);
             final PasswordUtility.PasswordCheckInfo passwordCheckInfo = verifyForm(pwmRequest, newUserForm, true);
             NewUserUtils.passwordCheckInfoToException(passwordCheckInfo);
             newUserBean.setNewUserForm(newUserForm);
@@ -616,10 +626,20 @@ public class NewUserServlet extends ControlledPwmServlet {
     private void forwardToFormPage(final PwmRequest pwmRequest, final NewUserBean newUserBean)
             throws ServletException, PwmUnrecoverableException, IOException
     {
-        final List<FormConfiguration> formConfiguration = getFormDefinition(pwmRequest);
+        final List<FormConfiguration> formConfigurations = getFormDefinition(pwmRequest);
         final NewUserProfile newUserProfile = getNewUserProfile(pwmRequest);
         final boolean promptForPassword = newUserProfile.readSettingAsBoolean(PwmSetting.NEWUSER_PROMPT_FOR_PASSWORD);
-        pwmRequest.addFormInfoToRequestAttr(formConfiguration, null, false, promptForPassword);
+        final Map<FormConfiguration,String> formData = new HashMap<>();
+        if (newUserBean.getRemoteInputData() != null) {
+            final Map<String,String> remoteData = newUserBean.getRemoteInputData();
+            for (final FormConfiguration formConfiguration : formConfigurations) {
+                if (remoteData.containsKey(formConfiguration.getName())) {
+                    formData.put(formConfiguration, remoteData.get(formConfiguration.getName()));
+                }
+            }
+        }
+
+        pwmRequest.addFormInfoToRequestAttr(formConfigurations, formData, false, promptForPassword);
 
         {
             final boolean showBack = !newUserBean.isUrlSpecifiedProfile()
@@ -629,5 +649,4 @@ public class NewUserServlet extends ControlledPwmServlet {
 
         pwmRequest.forwardToJsp(JspUrl.NEW_USER);
     }
-
 }

+ 14 - 7
src/main/java/password/pwm/http/servlet/newuser/NewUserTokenData.java

@@ -22,24 +22,31 @@
 
 package password.pwm.http.servlet.newuser;
 
-import password.pwm.http.bean.NewUserBean;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 
 import java.io.Serializable;
+import java.util.Map;
 
+@Getter
+@AllArgsConstructor
 class NewUserTokenData implements Serializable {
+
+    @SerializedName("id")
     private String profileID;
-    private NewUserBean.NewUserForm formData;
 
-    NewUserTokenData(final String profileID, final NewUserBean.NewUserForm formData) {
-        this.profileID = profileID;
-        this.formData = formData;
-    }
+    @SerializedName("f")
+    private NewUserForm formData;
+
+    @SerializedName("i")
+    private Map<String,String> injectionData;
 
     public String getProfileID() {
         return profileID;
     }
 
-    public NewUserBean.NewUserForm getFormData() {
+    public NewUserForm getFormData() {
         return formData;
     }
 }

+ 20 - 9
src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -43,6 +43,7 @@ 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.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -80,7 +81,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
-public class NewUserUtils {
+class NewUserUtils {
     private static PwmLogger LOGGER = password.pwm.util.logging.PwmLogger.forClass(NewUserUtils.class);
 
     private NewUserUtils() {
@@ -102,7 +103,7 @@ public class NewUserUtils {
     }
 
     static void createUser(
-            final NewUserBean.NewUserForm newUserForm,
+            final NewUserForm newUserForm,
             final PwmRequest pwmRequest,
             final String newUserDN
     )
@@ -124,7 +125,17 @@ public class NewUserUtils {
         }
 
         NewUserUtils.LOGGER.debug(pwmSession, "beginning createUser process for " + newUserDN);
-        final PasswordData userPassword = newUserForm.getNewUserPassword();
+
+        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
+        final boolean promptForPassword = newUserProfile.readSettingAsBoolean(PwmSetting.NEWUSER_PROMPT_FOR_PASSWORD);
+
+        final PasswordData userPassword;
+        if (promptForPassword) {
+            userPassword = newUserForm.getNewUserPassword();
+        } else {
+            final PwmPasswordPolicy pwmPasswordPolicy = newUserProfile.getNewUserPasswordPolicy(pwmRequest.getPwmApplication(), pwmRequest.getLocale());
+            userPassword = RandomPasswordGenerator.createRandomPassword(pwmRequest.getSessionLabel(), pwmPasswordPolicy, pwmRequest.getPwmApplication());
+        }
 
         // set up the user creation attributes
         final Map<String, String> createAttributes = NewUserFormUtils.getLdapDataFromNewUserForm(NewUserServlet.getNewUserProfile(pwmRequest), newUserForm);
@@ -152,7 +163,6 @@ public class NewUserUtils {
         }
 
         final ChaiUser theUser = ChaiFactory.createChaiUser(newUserDN, chaiProvider);
-        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
 
         final boolean useTempPw;
         {
@@ -168,8 +178,9 @@ public class NewUserUtils {
             NewUserUtils.LOGGER.trace(pwmSession, "will use temporary password process for new user entry: " + newUserDN);
             final PasswordData temporaryPassword;
             {
-                final RandomPasswordGenerator.RandomGeneratorConfig randomGeneratorConfig = new RandomPasswordGenerator.RandomGeneratorConfig();
-                randomGeneratorConfig.setPasswordPolicy(newUserProfile.getNewUserPasswordPolicy(pwmApplication, pwmRequest.getLocale()));
+                final RandomPasswordGenerator.RandomGeneratorConfig randomGeneratorConfig = RandomPasswordGenerator.RandomGeneratorConfig.builder()
+                        .passwordPolicy(newUserProfile.getNewUserPasswordPolicy(pwmApplication, pwmRequest.getLocale()))
+                        .build();
                 temporaryPassword = RandomPasswordGenerator.createRandomPassword(pwmSession.getLabel(), randomGeneratorConfig, pwmApplication);
             }
             final ChaiUser proxiedUser = ChaiFactory.createChaiUser(newUserDN, chaiProvider);
@@ -293,7 +304,7 @@ public class NewUserUtils {
 
     static String determineUserDN(
             final PwmRequest pwmRequest,
-            final NewUserBean.NewUserForm formValues
+            final NewUserForm formValues
     )
             throws PwmUnrecoverableException, ChaiUnavailableException
     {
@@ -405,7 +416,7 @@ public class NewUserUtils {
     static MacroMachine createMacroMachineForNewUser(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
-            final NewUserBean.NewUserForm newUserForm
+            final NewUserForm newUserForm
     )
             throws PwmUnrecoverableException
     {
@@ -445,7 +456,7 @@ public class NewUserUtils {
         }
 
         final Configuration config = pwmApplication.getConfig();
-        final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload(pwmRequest, newUserBean.getNewUserForm());
+        final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload(pwmRequest, newUserBean);
         final MacroMachine macroMachine = createMacroMachineForNewUser(pwmApplication, pwmRequest.getSessionLabel(), newUserBean.getNewUserForm());
 
         switch (tokenType) {

+ 2 - 2
src/main/java/password/pwm/http/state/SessionStateService.java

@@ -35,7 +35,7 @@ import password.pwm.svc.PwmService;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -179,7 +179,7 @@ public class SessionStateService implements PwmService {
         try {
             final E newBean = theClass.newInstance();
             newBean.setGuid(sessionGuid);
-            newBean.setTimestamp(new Date());
+            newBean.setTimestamp(Instant.now());
             return newBean;
         } catch (Exception e) {
             final String errorMsg = "unexpected error trying to instantiate bean class " + theClass.getName() + ": " + e.getMessage();

+ 4 - 3
src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -383,9 +383,10 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
             );
 
             // create random password for user
-            final RandomPasswordGenerator.RandomGeneratorConfig randomGeneratorConfig = new RandomPasswordGenerator.RandomGeneratorConfig();
-            randomGeneratorConfig.setSeedlistPhrases(RandomPasswordGenerator.DEFAULT_SEED_PHRASES);
-            randomGeneratorConfig.setPasswordPolicy(passwordPolicy);
+            final RandomPasswordGenerator.RandomGeneratorConfig randomGeneratorConfig = RandomPasswordGenerator.RandomGeneratorConfig.builder()
+                    .seedlistPhrases(RandomPasswordGenerator.DEFAULT_SEED_PHRASES)
+                    .passwordPolicy(passwordPolicy)
+                    .build();
 
             final PasswordData currentPass = RandomPasswordGenerator.createRandomPassword(sessionLabel, randomGeneratorConfig, pwmApplication);
 

+ 4 - 1
src/main/java/password/pwm/svc/PwmServiceManager.java

@@ -34,6 +34,7 @@ import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapConnectionService;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheService;
+import password.pwm.svc.cluster.ClusterService;
 import password.pwm.svc.event.AuditService;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.report.ReportService;
@@ -45,6 +46,7 @@ import password.pwm.svc.wordlist.SeedlistManager;
 import password.pwm.svc.wordlist.SharedHistoryManager;
 import password.pwm.svc.wordlist.WordlistManager;
 import password.pwm.util.VersionChecker;
+import password.pwm.util.db.DatabaseService;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.CrService;
@@ -72,7 +74,7 @@ public class PwmServiceManager {
     public enum PwmServiceClassEnum {
         SecureService(          SecureService.class,             true),
         LdapConnectionService(  LdapConnectionService.class,     true),
-        DatabaseService(        password.pwm.util.db.DatabaseService.class,           true),
+        DatabaseService(        DatabaseService.class,           true),
         SharedHistoryManager(   SharedHistoryManager.class,      false),
         AuditService(           AuditService.class,              false),
         StatisticsManager(      StatisticsManager.class,         false),
@@ -93,6 +95,7 @@ public class PwmServiceManager {
         SessionTrackService(    SessionTrackService.class,       false),
         SessionStateSvc(        SessionStateService.class,       false),
         UserSearchEngine(       UserSearchEngine.class,          true),
+        ClusterService(         ClusterService.class,            false),
 
         ;
 

+ 35 - 0
src/main/java/password/pwm/svc/cluster/ClusterProvider.java

@@ -0,0 +1,35 @@
+/*
+ * 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
+ */
+
+package password.pwm.svc.cluster;
+
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.util.List;
+
+public interface ClusterProvider {
+    void close();
+
+    boolean isMaster();
+
+    List<NodeInfo> nodes() throws PwmUnrecoverableException;
+}

+ 106 - 0
src/main/java/password/pwm/svc/cluster/ClusterService.java

@@ -0,0 +1,106 @@
+/*
+ * 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
+ */
+
+package password.pwm.svc.cluster;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.HealthRecord;
+import password.pwm.svc.PwmService;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ClusterService implements PwmService {
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass(ClusterService.class);
+
+    private PwmApplication pwmApplication;
+    private STATUS status = STATUS.NEW;
+    private ClusterProvider clusterProvider;
+
+    @Override
+    public STATUS status()
+    {
+        return status;
+    }
+
+    @Override
+    public void init(final PwmApplication pwmApplication) throws PwmException
+    {
+        status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
+
+        try {
+            if (this.pwmApplication.getConfig().hasDbConfigured()) {
+                clusterProvider = new DatabaseClusterProvider(pwmApplication);
+            }
+        } catch (PwmException e) {
+            LOGGER.error("error starting up cluster provider service: " + e.getMessage());
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        status = STATUS.OPEN;
+    }
+
+    @Override
+    public void close()
+    {
+        if (clusterProvider != null) {
+            clusterProvider.close();
+            clusterProvider = null;
+        }
+        clusterProvider = null;
+        status = STATUS.CLOSED;
+    }
+
+    @Override
+    public List<HealthRecord> healthCheck()
+    {
+        return null;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo()
+    {
+        return null;
+    }
+
+    public boolean isMaster() {
+        if (clusterProvider != null) {
+            return clusterProvider.isMaster();
+        }
+
+        return false;
+    }
+
+    public List<NodeInfo> nodes() throws PwmUnrecoverableException
+    {
+        if (status == STATUS.OPEN && clusterProvider != null) {
+            return clusterProvider.nodes();
+        }
+        return Collections.emptyList();
+    }
+}

+ 240 - 0
src/main/java/password/pwm/svc/cluster/DatabaseClusterProvider.java

@@ -0,0 +1,240 @@
+/*
+ * 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
+ */
+
+package password.pwm.svc.cluster;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.db.DatabaseService;
+import password.pwm.util.db.DatabaseTable;
+import password.pwm.util.java.ClosableIterator;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+class DatabaseClusterProvider implements ClusterProvider {
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass(DatabaseClusterProvider.class);
+
+    private static final DatabaseTable TABLE = DatabaseTable.CLUSTER_STATE;
+
+
+    private static final String KEY_PREFIX_NODE = "node-";
+
+    private final PwmApplication pwmApplication;
+    private final DatabaseService databaseService;
+    private final ScheduledExecutorService executorService;
+
+    private ErrorInformation lastError;
+
+    private final Map<String,DatabaseStoredNodeData> nodeDatas = new ConcurrentHashMap<>();
+
+    private final DatabaseClusterSettings settings;
+
+    DatabaseClusterProvider(final PwmApplication pwmApplication) throws PwmUnrecoverableException
+    {
+        this.pwmApplication = pwmApplication;
+        this.settings = DatabaseClusterSettings.fromConfig(pwmApplication.getConfig());
+
+        if (!settings.isEnable()) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,"database clustering is not enabled via app property"));
+        }
+
+        this.databaseService = pwmApplication.getDatabaseService();
+        this.executorService = JavaHelper.makeSingleThreadExecutorService(pwmApplication, DatabaseClusterProvider.class);
+
+        final long intervalSeconds = settings.getHeartbeatInterval().getTotalSeconds();
+
+        this.executorService.scheduleAtFixedRate(
+                new HeartbeatProcess(),
+                1,
+                intervalSeconds,
+                TimeUnit.SECONDS
+        );
+    }
+
+    @Override
+    public void close() {
+        JavaHelper.closeAndWaitExecutor(executorService, new TimeDuration(1, TimeUnit.SECONDS));
+    }
+
+
+    @Override
+    public List<NodeInfo> nodes() throws PwmUnrecoverableException
+    {
+        final Map<String,NodeInfo> returnObj = new TreeMap<>();
+        final String configHash = pwmApplication.getConfig().configurationHash();
+        for (final DatabaseStoredNodeData storedNodeData : nodeDatas.values()) {
+            final boolean configMatch = configHash.equals(storedNodeData.getConfigHash());
+            final boolean timedOut = isTimedOut(storedNodeData);
+
+            final NodeInfo.NodeState nodeState = isMaster(storedNodeData)
+                    ? NodeInfo.NodeState.master
+                    : timedOut
+                    ? NodeInfo.NodeState.offline
+                    : NodeInfo.NodeState.online;
+
+            final Instant startupTime = nodeState == NodeInfo.NodeState.offline
+                    ? null
+                    : storedNodeData.getStartupTimestamp();
+
+
+            final NodeInfo nodeInfo = new NodeInfo(
+                    storedNodeData.getInstanceID(),
+                    storedNodeData.getTimestamp(),
+                    startupTime,
+                    nodeState,
+                    configMatch
+            );
+            returnObj.put(nodeInfo.getInstanceID(), nodeInfo);
+        }
+
+        return Collections.unmodifiableList(new ArrayList<>(returnObj.values()));
+    }
+
+
+    private String masterInstanceId() {
+        final List<DatabaseStoredNodeData> copiedDatas = new ArrayList<>(nodeDatas.values());
+        if (copiedDatas.isEmpty()) {
+            return null;
+        }
+
+        String masterID = null;
+        Instant eldestRecord = Instant.now();
+
+        for (final DatabaseStoredNodeData nodeData : copiedDatas) {
+            if (!isTimedOut(nodeData)) {
+                if (nodeData.getStartupTimestamp().isBefore(eldestRecord)) {
+                    eldestRecord = nodeData.getStartupTimestamp();
+                    masterID = nodeData.getInstanceID();
+                }
+            }
+        }
+        return masterID;
+    }
+
+    @Override
+    public boolean isMaster() {
+        final String myID = pwmApplication.getInstanceID();
+        final String masterID = masterInstanceId();
+        return myID.equals(masterID);
+    }
+
+    private boolean isMaster(final DatabaseStoredNodeData databaseStoredNodeData) {
+        final String masterID = masterInstanceId();
+        return databaseStoredNodeData.getInstanceID().equals(masterID);
+    }
+
+    private String dbKeyForStoredNode(final DatabaseStoredNodeData storedNodeData) throws PwmUnrecoverableException
+    {
+        final String instanceID = storedNodeData.getInstanceID();
+        final String hash = pwmApplication.getSecureService().hash(instanceID);
+        final String truncatedHash = hash.length() > 64
+                ? hash.substring(0, 64)
+                : hash;
+
+        return KEY_PREFIX_NODE + truncatedHash;
+    }
+
+    private boolean isTimedOut(final DatabaseStoredNodeData storedNodeData) {
+        final TimeDuration age = TimeDuration.fromCurrent(storedNodeData.getTimestamp());
+        return age.isLongerThan(settings.getNodeTimeout());
+    }
+
+    private class HeartbeatProcess implements Runnable {
+        public void run() {
+            writeNodeStatus();
+            readNodeStatuses();
+            purgeOutdatedNodes();
+        }
+
+        void writeNodeStatus()
+        {
+            try {
+                final DatabaseStoredNodeData storedNodeData = DatabaseStoredNodeData.makeNew(pwmApplication);
+                final String key = dbKeyForStoredNode(storedNodeData);
+                final String value = JsonUtil.serialize(storedNodeData);
+                databaseService.getAccessor().put(TABLE, key, value);
+            } catch (PwmException e) {
+                final String errorMsg = "error writing database cluster heartbeat: " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE, errorMsg);
+                lastError = errorInformation;
+                LOGGER.error(lastError);
+            }
+        }
+
+        void readNodeStatuses()
+        {
+            try (ClosableIterator<String> tableIterator = databaseService.getAccessor().iterator(TABLE)) {
+                while (tableIterator.hasNext()) {
+                    final String dbKey = tableIterator.next();
+                    if (dbKey.startsWith(KEY_PREFIX_NODE)) {
+                        final String rawValueInDb = databaseService.getAccessor().get(TABLE, dbKey);
+                        final DatabaseStoredNodeData nodeDataInDb = JsonUtil.deserialize(rawValueInDb, DatabaseStoredNodeData.class);
+                        nodeDatas.put(nodeDataInDb.getInstanceID(), nodeDataInDb);
+                    }
+                }
+            } catch (PwmException e) {
+                final String errorMsg = "error reading database node statuses: " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE, errorMsg);
+                lastError = errorInformation;
+                LOGGER.error(lastError);
+            }
+        }
+
+        void purgeOutdatedNodes() {
+            for (final DatabaseStoredNodeData storedNodeData : nodeDatas.values()) {
+                final TimeDuration recordAge = TimeDuration.fromCurrent(storedNodeData.getTimestamp());
+                final String instanceID = storedNodeData.getInstanceID();
+
+                if (recordAge.isLongerThan(settings.getNodePurgeInterval())) {
+                    // purge outdated records
+                    LOGGER.debug("purging outdated node reference to instanceID '" + instanceID + "'");
+
+                    try {
+                        databaseService.getAccessor().remove(TABLE, dbKeyForStoredNode(storedNodeData));
+                    } catch (PwmException e) {
+                        final String errorMsg = "error purging outdated node reference: " + e.getMessage();
+                        final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE, errorMsg);
+                        lastError = errorInformation;
+                        LOGGER.error(lastError);
+                    }
+                    nodeDatas.remove(instanceID);
+                }
+            }
+        }
+    }
+}

+ 50 - 0
src/main/java/password/pwm/svc/cluster/DatabaseClusterSettings.java

@@ -0,0 +1,50 @@
+/*
+ * 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
+ */
+
+package password.pwm.svc.cluster;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.AppProperty;
+import password.pwm.config.Configuration;
+import password.pwm.util.java.TimeDuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+class DatabaseClusterSettings {
+    private final boolean enable;
+    private final TimeDuration heartbeatInterval;
+    private final TimeDuration nodeTimeout;
+    private final TimeDuration nodePurgeInterval;
+
+    static DatabaseClusterSettings fromConfig(final Configuration configuration) {
+        return new DatabaseClusterSettings(
+                Boolean.parseBoolean(configuration.readAppProperty(AppProperty.CLUSTER_DB_ENABLE)),
+                new TimeDuration( Integer.parseInt(configuration.readAppProperty(AppProperty.CLUSTER_DB_HEARTBEAT_SECONDS)), TimeUnit.SECONDS),
+                new TimeDuration( Integer.parseInt(configuration.readAppProperty(AppProperty.CLUSTER_DB_NODE_TIMEOUT_SECONDS)), TimeUnit.SECONDS),
+                new TimeDuration( Integer.parseInt(configuration.readAppProperty(AppProperty.CLUSTER_DB_NODE_PURGE_SECONDS)), TimeUnit.SECONDS)
+        );
+    }
+}

+ 54 - 0
src/main/java/password/pwm/svc/cluster/DatabaseStoredNodeData.java

@@ -0,0 +1,54 @@
+/*
+ * 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
+ */
+
+package password.pwm.svc.cluster;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+class DatabaseStoredNodeData implements Serializable {
+    private Instant timestamp;
+    private Instant startupTimestamp;
+    private String instanceID;
+    private String guid;
+    private String configHash;
+
+    static DatabaseStoredNodeData makeNew(final PwmApplication pwmApplication)
+            throws PwmUnrecoverableException
+    {
+        return new DatabaseStoredNodeData(
+                Instant.now(),
+                pwmApplication.getStartupTime(),
+                pwmApplication.getInstanceID(),
+                pwmApplication.getInstanceNonce(),
+                pwmApplication.getConfig().configurationHash()
+        );
+    }
+}

+ 18 - 13
src/main/java/password/pwm/util/db/DatabaseClusterService.java → src/main/java/password/pwm/svc/cluster/NodeInfo.java

@@ -20,22 +20,27 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util.db;
+package password.pwm.svc.cluster;
 
-public class DatabaseClusterService {
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 
+import java.io.Serializable;
+import java.time.Instant;
 
+@Getter
+@AllArgsConstructor(access = AccessLevel.PACKAGE)
+public class NodeInfo implements Serializable {
+    private String instanceID;
+    private Instant lastSeen;
+    private Instant startupTime;
+    private NodeState nodeState;
+    private boolean configMatch;
 
-    private static final String KEY_ENGINE_START_PREFIX = "engine-start-";
-
-    private void heartbeat() {
-        /*
-        try {
-            put(DatabaseTable.PWM_META, KEY_ENGINE_START_PREFIX + instanceID, JavaHelper.toIsoDate(new java.util.Date()));
-        } catch (DatabaseException e) {
-            final String errorMsg = "error writing engine start time value: " + e.getMessage();
-            throw new DatabaseException(new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg));
-        }
-        */
+    enum NodeState {
+        master,
+        online,
+        offline
     }
 }

+ 4 - 13
src/main/java/password/pwm/svc/event/AuditService.java

@@ -214,7 +214,7 @@ public class AuditService implements PwmService {
         return userHistoryStore.readUserHistory(userInfoBean);
     }
 
-    protected void sendAsEmail(final AuditRecord record)
+    private void sendAsEmail(final AuditRecord record)
             throws PwmUnrecoverableException
     {
         if (record == null || record.getEventCode() == null) {
@@ -227,14 +227,14 @@ public class AuditService implements PwmService {
         switch (record.getEventCode().getType()) {
             case SYSTEM:
                 for (final String toAddress : settings.getSystemEmailAddresses()) {
-                    sendAsEmail(pwmApplication, null, record, toAddress, settings.getAlertFromAddress());
+                    sendAsEmail(pwmApplication, record, toAddress, settings.getAlertFromAddress());
                 }
                 break;
 
             case USER:
             case HELPDESK:
                 for (final String toAddress : settings.getUserEmailAddresses()) {
-                    sendAsEmail(pwmApplication, null, record, toAddress, settings.getAlertFromAddress());
+                    sendAsEmail(pwmApplication, record, toAddress, settings.getAlertFromAddress());
                 }
                 break;
 
@@ -246,7 +246,6 @@ public class AuditService implements PwmService {
 
     private static void sendAsEmail(
             final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
             final AuditRecord record,
             final String toAddress,
             final String fromAddress
@@ -254,7 +253,7 @@ public class AuditService implements PwmService {
     )
             throws PwmUnrecoverableException
     {
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmApplication, sessionLabel);
+        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmApplication, SessionLabel.AUDITING_SESSION_LABEL);
 
         String subject = macroMachine.expandMacros(pwmApplication.getConfig().readAppProperty(AppProperty.AUDIT_EVENTS_EMAILSUBJECT));
         subject = subject.replace("%EVENT%", record.getEventCode().getLocalizedString(pwmApplication.getConfig(), PwmConstants.DEFAULT_LOCALE));
@@ -270,14 +269,6 @@ public class AuditService implements PwmService {
         pwmApplication.getEmailQueue().submitEmail(emailItem, null, macroMachine);
     }
 
-    public int vaultSize() {
-        if (status != STATUS.OPEN || auditVault == null) {
-            return -1;
-        }
-
-        return auditVault.size();
-    }
-
     public Instant eldestVaultRecord() {
         if (status != STATUS.OPEN || auditVault == null) {
             return null;

+ 1 - 1
src/main/java/password/pwm/svc/event/SyslogAuditService.java

@@ -96,7 +96,7 @@ public class SyslogAuditService {
             syslogInstance = makeSyslogInstance(syslogConfig);
             LOGGER.trace("queued service running for " + syslogConfig);
         } catch (IllegalArgumentException e) {
-            LOGGER.error("e9rror parsing syslog configuration for '" + syslogConfigString + "', error: " + e.getMessage());
+            LOGGER.error("error parsing syslog configuration for '" + syslogConfigString + "', error: " + e.getMessage());
         }
 
         final WorkQueueProcessor.Settings settings = WorkQueueProcessor.Settings.builder()

+ 6 - 6
src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java

@@ -331,7 +331,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService {
         if (storedValue != null) {
             return storedValue;
         }
-        return new StoredWordlistDataBean.Builder().create();
+        return StoredWordlistDataBean.builder().build();
     }
 
     void writeMetadata(final StoredWordlistDataBean metadataBean) {
@@ -367,7 +367,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService {
         if (wlStatus == STATUS.OPEN) {
             executorService.schedule(() -> {
                 try {
-                    writeMetadata(new StoredWordlistDataBean.Builder().create());
+                    writeMetadata(StoredWordlistDataBean.builder().build());
                     populationManager.checkPopulation();
                 } catch (Exception e) {
                     LOGGER.error("error during clear operation: " + e.getMessage());
@@ -407,7 +407,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService {
             } else {
                 if (readMetadata().getSource() == StoredWordlistDataBean.Source.AutoImport) {
                     LOGGER.trace("source list is from auto-import, but not currently configured for auto-import; clearing stored data");
-                    writeMetadata(new StoredWordlistDataBean.Builder().create()); // clear previous auto-import wll
+                    writeMetadata(StoredWordlistDataBean.builder().build()); // clear previous auto-import wll
                 }
             }
 
@@ -462,9 +462,9 @@ abstract class AbstractWordlist implements Wordlist, PwmService {
                 }
 
                 { // reset the wordlist metadata
-                    final StoredWordlistDataBean storedWordlistDataBean = new StoredWordlistDataBean.Builder()
-                            .setSource(source)
-                            .create();
+                    final StoredWordlistDataBean storedWordlistDataBean = StoredWordlistDataBean.builder()
+                            .source(source)
+                            .build();
                     writeMetadata(storedWordlistDataBean);
                 }
 

+ 9 - 8
src/main/java/password/pwm/svc/wordlist/Populator.java

@@ -39,6 +39,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
+import java.time.Instant;
 import java.util.Date;
 import java.util.Map;
 import java.util.TreeMap;
@@ -139,7 +140,7 @@ class Populator {
 
     void populate() throws IOException, LocalDBException, PwmUnrecoverableException {
         try {
-            rootWordlist.writeMetadata(new StoredWordlistDataBean.Builder().setSource(source).create());
+            rootWordlist.writeMetadata(StoredWordlistDataBean.builder().source(source).build());
             running = true;
             init();
 
@@ -240,13 +241,13 @@ class Populator {
         sb.append(" population complete, added ").append(wordlistSize);
         sb.append(" total words in ").append(new TimeDuration(overallStats.getElapsedSeconds() * 1000).asCompactString());
         {
-            final StoredWordlistDataBean storedWordlistDataBean = new StoredWordlistDataBean.Builder()
-                    .setSha1hash(JavaHelper.binaryArrayToHex(checksumInputStream.closeAndFinalChecksum()))
-                    .setSize(wordlistSize)
-                    .setStoreDate(new Date())
-                    .setSource(source)
-                    .setCompleted(!abortFlag)
-                    .create();
+            final StoredWordlistDataBean storedWordlistDataBean = StoredWordlistDataBean.builder()
+                    .sha1hash(JavaHelper.binaryArrayToHex(checksumInputStream.closeAndFinalChecksum()))
+                    .size(wordlistSize)
+                    .storeDate(Instant.now())
+                    .source(source)
+                    .completed(!abortFlag)
+                    .build();
             rootWordlist.writeMetadata(storedWordlistDataBean);
         }
         LOGGER.info(sb.toString());

+ 7 - 78
src/main/java/password/pwm/svc/wordlist/StoredWordlistDataBean.java

@@ -22,13 +22,18 @@
 
 package password.pwm.svc.wordlist;
 
+import lombok.Builder;
+import lombok.Getter;
+
 import java.io.Serializable;
-import java.util.Date;
+import java.time.Instant;
 
+@Getter
+@Builder
 public class StoredWordlistDataBean implements Serializable {
     private boolean completed;
     private Source source;
-    private Date storeDate;
+    private Instant storeDate;
     private String sha1hash;
     private int size;
 
@@ -49,80 +54,4 @@ public class StoredWordlistDataBean implements Serializable {
             return label;
         }
     }
-
-    public boolean isCompleted() {
-        return completed;
-    }
-
-    public Source getSource() {
-        return source;
-    }
-
-    public Date getStoreDate() {
-        return storeDate;
-    }
-
-    public String getSha1hash() {
-        return sha1hash;
-    }
-
-    public int getSize() {
-        return size;
-    }
-
-    public StoredWordlistDataBean(final boolean completed, final Source source, final Date storeDate, final String sha1hash, final int size) {
-        this.completed = completed;
-        this.source = source;
-        this.storeDate = storeDate;
-        this.sha1hash = sha1hash;
-        this.size = size;
-    }
-
-    public static class Builder {
-        private boolean completed;
-        private Source source;
-        private Date storeDate;
-        private String sha1hash;
-        private int size;
-
-        public Builder() {
-        }
-
-        public Builder(final StoredWordlistDataBean source) {
-            this.completed = source.completed;
-            this.source = source.source;
-            this.storeDate = source.storeDate;
-            this.sha1hash = source.sha1hash;
-            this.size = source.size;
-        }
-
-        public Builder setCompleted(final boolean completed) {
-            this.completed = completed;
-            return this;
-        }
-
-        public Builder setSource(final Source source) {
-            this.source = source;
-            return this;
-        }
-
-        public Builder setStoreDate(final Date storeDate) {
-            this.storeDate = storeDate;
-            return this;
-        }
-
-        public Builder setSha1hash(final String sha1hash) {
-            this.sha1hash = sha1hash;
-            return this;
-        }
-
-        public Builder setSize(final int size) {
-            this.size = size;
-            return this;
-        }
-
-        public StoredWordlistDataBean create() {
-            return new StoredWordlistDataBean(completed, source, storeDate, sha1hash, size);
-        }
-    }
 }

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

@@ -31,6 +31,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
+import javax.servlet.http.HttpServletRequest;
 import java.io.Serializable;
 
 /**
@@ -55,7 +56,14 @@ public class BasicAuthInfo implements Serializable {
             final PwmApplication pwmApplication,
             final PwmRequest pwmRequest
     ) {
-        final String authHeader = pwmRequest.readHeaderValueAsString(HttpHeader.Authorization);
+        return parseAuthHeader(pwmApplication, pwmRequest.getHttpServletRequest());
+    }
+
+    public static BasicAuthInfo parseAuthHeader(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest httpServletRequest
+    ) {
+        final String authHeader = httpServletRequest.getHeader(HttpHeader.Authorization.getHttpName());
 
         if (authHeader != null) {
             if (authHeader.contains(PwmConstants.HTTP_BASIC_AUTH_PREFIX)) {
@@ -73,7 +81,7 @@ public class BasicAuthInfo implements Serializable {
                     //   "cn=user,o=company:chpass" or "user:chpass"
                     return parseHeaderString(decoded);
                 } catch (Exception e) {
-                    LOGGER.debug(pwmRequest, "error decoding auth header");
+                    LOGGER.debug("error decoding auth header");
                 }
             }
         }

+ 97 - 102
src/main/java/password/pwm/util/RandomPasswordGenerator.java

@@ -23,6 +23,8 @@
 package password.pwm.util;
 
 import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
+import lombok.Builder;
+import lombok.Getter;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
@@ -100,8 +102,9 @@ public class RandomPasswordGenerator {
     )
             throws PwmUnrecoverableException
     {
-        final RandomGeneratorConfig randomGeneratorConfig = new RandomGeneratorConfig();
-        randomGeneratorConfig.setPasswordPolicy(passwordPolicy);
+        final RandomGeneratorConfig randomGeneratorConfig = RandomGeneratorConfig.builder()
+                .passwordPolicy(passwordPolicy)
+                .build();
 
         return createRandomPassword(
                 sessionLabel,
@@ -135,28 +138,34 @@ public class RandomPasswordGenerator {
     {
         final long startTimeMS = System.currentTimeMillis();
 
-        validateSettings(pwmApplication, randomGeneratorConfig);
+        randomGeneratorConfig.validateSettings(pwmApplication);
 
-        if (randomGeneratorConfig.getSeedlistPhrases() == null || randomGeneratorConfig.getSeedlistPhrases().isEmpty()) {
-            Set<String> seeds = DEFAULT_SEED_PHRASES;
-
-            final SeedlistManager seedlistManager = pwmApplication.getSeedlistManager();
-            if (seedlistManager != null && seedlistManager.status() == PwmService.STATUS.OPEN && seedlistManager.size() > 0) {
-                seeds = new HashSet<>();
-                int safetyCounter = 0;
-                while (seeds.size() < 10 && safetyCounter < 100) {
-                    safetyCounter++;
-                    final String randomWord = seedlistManager.randomSeed();
-                    if (randomWord != null) {
-                        seeds.add(randomWord);
+        final RandomGeneratorConfig effectiveConfig;
+        {
+            if (randomGeneratorConfig.getSeedlistPhrases() == null || randomGeneratorConfig.getSeedlistPhrases().isEmpty()) {
+                Set<String> seeds = DEFAULT_SEED_PHRASES;
+
+                final SeedlistManager seedlistManager = pwmApplication.getSeedlistManager();
+                if (seedlistManager != null && seedlistManager.status() == PwmService.STATUS.OPEN && seedlistManager.size() > 0) {
+                    seeds = new HashSet<>();
+                    int safetyCounter = 0;
+                    while (seeds.size() < 10 && safetyCounter < 100) {
+                        safetyCounter++;
+                        final String randomWord = seedlistManager.randomSeed();
+                        if (randomWord != null) {
+                            seeds.add(randomWord);
+                        }
                     }
                 }
+                effectiveConfig = randomGeneratorConfig.toBuilder()
+                        .seedlistPhrases(seeds)
+                        .build();
+            } else {
+                effectiveConfig = randomGeneratorConfig;
             }
-            randomGeneratorConfig.setSeedlistPhrases(seeds);
         }
 
-
-        final SeedMachine seedMachine = new SeedMachine(normalizeSeeds(randomGeneratorConfig.getSeedlistPhrases()));
+        final SeedMachine seedMachine = new SeedMachine(normalizeSeeds(effectiveConfig.getSeedlistPhrases()));
 
         int tryCount = 0;
         final StringBuilder password = new StringBuilder();
@@ -165,24 +174,24 @@ public class RandomPasswordGenerator {
         final PwmPasswordPolicy randomGenPolicy;
         {
             final Map<String, String> newPolicyMap = new HashMap<>();
-            newPolicyMap.putAll(randomGeneratorConfig.getPasswordPolicy().getPolicyMap());
+            newPolicyMap.putAll(effectiveConfig.getPasswordPolicy().getPolicyMap());
 
-            final String max = newPolicyMap.put(PwmPasswordRule.MaximumLength.getKey(), String.valueOf(randomGeneratorConfig.getMaximumLength()));
+            final String max = newPolicyMap.put(PwmPasswordRule.MaximumLength.getKey(), String.valueOf(effectiveConfig.getMaximumLength()));
 
-            if (randomGeneratorConfig.getMinimumLength() > randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumLength)) {
-                newPolicyMap.put(PwmPasswordRule.MinimumLength.getKey(), String.valueOf(randomGeneratorConfig.getMinimumLength()));
+            if (effectiveConfig.getMinimumLength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumLength)) {
+                newPolicyMap.put(PwmPasswordRule.MinimumLength.getKey(), String.valueOf(effectiveConfig.getMinimumLength()));
             }
-            if (randomGeneratorConfig.getMaximumLength() < randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MaximumLength)) {
-                newPolicyMap.put(PwmPasswordRule.MaximumLength.getKey(), String.valueOf(randomGeneratorConfig.getMaximumLength()));
+            if (effectiveConfig.getMaximumLength() < effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MaximumLength)) {
+                newPolicyMap.put(PwmPasswordRule.MaximumLength.getKey(), String.valueOf(effectiveConfig.getMaximumLength()));
             }
-            if (randomGeneratorConfig.getMinimumStrength() > randomGeneratorConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumStrength)) {
-                newPolicyMap.put(PwmPasswordRule.MinimumStrength.getKey(), String.valueOf(randomGeneratorConfig.getMinimumStrength()));
+            if (effectiveConfig.getMinimumStrength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumStrength)) {
+                newPolicyMap.put(PwmPasswordRule.MinimumStrength.getKey(), String.valueOf(effectiveConfig.getMinimumStrength()));
             }
             randomGenPolicy = PwmPasswordPolicy.createPwmPasswordPolicy(newPolicyMap);
         }
 
         // initial creation
-        password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
+        password.append(generateNewPassword(seedMachine, effectiveConfig.getMinimumLength()));
 
         // read a rule validator
         final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(pwmApplication, randomGenPolicy);
@@ -197,7 +206,7 @@ public class RandomPasswordGenerator {
 
             if (tryCount % JITTER_COUNT == 0) {
                 password.delete(0,password.length());
-                password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
+                password.append(generateNewPassword(seedMachine, effectiveConfig.getMinimumLength()));
             }
 
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
@@ -208,7 +217,7 @@ public class RandomPasswordGenerator {
             } else if (checkPasswordAgainstDisallowedHttpValues(pwmApplication.getConfig(), password.toString())) {
                 validPassword = false;
                 password.delete(0, password.length());
-                password.append(generateNewPassword(seedMachine, randomGeneratorConfig.getMinimumLength()));
+                password.append(generateNewPassword(seedMachine, effectiveConfig.getMinimumLength()));
             }
         }
 
@@ -544,99 +553,85 @@ public class RandomPasswordGenerator {
         return newSeeds.isEmpty() ? DEFAULT_SEED_PHRASES : newSeeds;
     }
 
+    @Getter
+    @Builder(toBuilder = true)
     public static class RandomGeneratorConfig {
-        public static final int DEFAULT_MINIMUM_LENGTH = 6;
-        public static final int DEFAULT_MAXIMUM_LENGTH = 16;
-        public static final int DEFAULT_DESIRED_STRENGTH = 45;
+        private static final int DEFAULT_MINIMUM_LENGTH = 6;
+        private static final int DEFAULT_MAXIMUM_LENGTH = 16;
+        private static final int DEFAULT_DESIRED_STRENGTH = 45;
 
-        public static final int MINIMUM_STRENGTH = 0;
-        public static final int MAXIMUM_STRENGTH = 100;
+        private static final int MINIMUM_STRENGTH = 0;
+        private static final int MAXIMUM_STRENGTH = 100;
 
+        /**
+         * A set of phrases (Strings) used to generate the RANDOM passwords.  There must be enough
+         * values in the phrases to build a random password that meets rule requirements
+         */
+        @Builder.Default
         private Collection<String> seedlistPhrases = Collections.emptySet();
-        private int minimumLength = DEFAULT_MINIMUM_LENGTH;
-        private int maximumLength = DEFAULT_MAXIMUM_LENGTH;
-        private int minimumStrength = DEFAULT_DESIRED_STRENGTH;
-        private PwmPasswordPolicy passwordPolicy = PwmPasswordPolicy.defaultPolicy();
-
-        public Collection<String> getSeedlistPhrases() {
-            return seedlistPhrases;
-        }
 
         /**
-         * @param seedlistPhrases A set of phrases (Strings) used to generate the RANDOM passwords.  There must be enough
-         *                        values in the phrases to build a resonably RANDOM password that meets rule requirements
+         * The minimum length desired for the password.  The algorithm will attempt to make
+         * the returned value at least this long, but it is not guaranteed.
          */
-        public void setSeedlistPhrases(final Collection<String> seedlistPhrases) {
-            this.seedlistPhrases = seedlistPhrases;
-        }
+        @Builder.Default
+        private int minimumLength = DEFAULT_MINIMUM_LENGTH;
 
-        public int getMinimumLength() {
-            return minimumLength;
-        }
+        @Builder.Default
+        private int maximumLength = DEFAULT_MAXIMUM_LENGTH;
 
         /**
-         * @param minimumLength The minimum length desired for the password.  The algorith will attempt to make
-         *                      the returned value at least this long, but it is not guarenteed.
+         * @param minimumStrength The minimum length desired strength.  The algorithm will attempt to make
+         *                        the returned value at least this strong, but it is not guaranteed.
          */
-        public void setMinimumLength(final int minimumLength) {
-            this.minimumLength = minimumLength;
-        }
+        @Builder.Default
+        private int minimumStrength = DEFAULT_DESIRED_STRENGTH;
 
-        public int getMaximumLength() {
-            return maximumLength;
-        }
+        @Builder.Default
+        private PwmPasswordPolicy passwordPolicy = PwmPasswordPolicy.defaultPolicy();
 
-        public void setMaximumLength(final int maximumLength) {
-            this.maximumLength = maximumLength;
-        }
 
-        public int getMinimumStrength() {
-            return minimumStrength;
+        public int getMaximumLength() {
+            int policyMax = this.maximumLength;
+            if (this.getPasswordPolicy() != null) {
+                policyMax = this.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MaximumLength);
+            }
+            return Math.min(this.maximumLength, policyMax);
         }
 
-        /**
-         * @param minimumStrength The minimum length desired strength.  The algorith will attempt to make
-         *                        the returned value at least this strong, but it is not guarenteed.
-         */
-        public void setMinimumStrength(final int minimumStrength) {
-            int desiredStrength = minimumStrength > MAXIMUM_STRENGTH ? MAXIMUM_STRENGTH : minimumStrength;
-            desiredStrength = desiredStrength < MINIMUM_STRENGTH ? MINIMUM_STRENGTH : desiredStrength;
-            this.minimumStrength = desiredStrength;
+        public int getMinimumStrength() {
+            int policyMin = this.minimumLength;
+            if (this.getPasswordPolicy() != null) {
+                policyMin = this.getPasswordPolicy().getRuleHelper().readIntValue(PwmPasswordRule.MinimumLength);
+            }
+            return Math.max(this.minimumLength, policyMin);
         }
 
-        public PwmPasswordPolicy getPasswordPolicy() {
-            return passwordPolicy;
-        }
+        void validateSettings(final PwmApplication pwmApplication)
+                throws PwmUnrecoverableException
+        {
+            final int maxLength = Integer.parseInt(
+                    pwmApplication.getConfig().readAppProperty(AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH));
+            if (this.getMinimumLength() > maxLength) {
+                throw new PwmUnrecoverableException(new ErrorInformation(
+                        PwmError.ERROR_UNKNOWN,
+                        "minimum random generated password length exceeds preset random generator threshold"
+                ));
+            }
 
-        public void setPasswordPolicy(final PwmPasswordPolicy passwordPolicy) {
-            this.passwordPolicy = passwordPolicy;
-        }
-    }
+            if (this.getMaximumLength() > maxLength) {
+                throw new PwmUnrecoverableException(new ErrorInformation(
+                        PwmError.ERROR_UNKNOWN,
+                        "maximum random generated password length exceeds preset random generator threshold"
+                ));
+            }
 
-    public static void validateSettings(final PwmApplication pwmApplication, final RandomGeneratorConfig randomGeneratorConfig)
-            throws PwmUnrecoverableException
-    {
-        final int maxLength = Integer.parseInt(
-                pwmApplication.getConfig().readAppProperty(AppProperty.PASSWORD_RANDOMGEN_MAX_LENGTH));
-        if (randomGeneratorConfig.getMinimumLength() > maxLength) {
-            throw new PwmUnrecoverableException(new ErrorInformation(
-                    PwmError.ERROR_UNKNOWN,
-                    "minimum random generated password length exceeds preset random generator threshold"
-            ));
-        }
-
-        if (randomGeneratorConfig.getMaximumLength() > maxLength) {
-            throw new PwmUnrecoverableException(new ErrorInformation(
-                    PwmError.ERROR_UNKNOWN,
-                    "maximum random generated password length exceeds preset random generator threshold"
-            ));
-        }
-
-        if (randomGeneratorConfig.getMinimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH) {
-            throw new PwmUnrecoverableException(new ErrorInformation(
-                    PwmError.ERROR_UNKNOWN,
-                    "minimum random generated password strength exceeds maximum possible"
-            ));
+            if (this.getMinimumStrength() > RandomGeneratorConfig.MAXIMUM_STRENGTH) {
+                throw new PwmUnrecoverableException(new ErrorInformation(
+                        PwmError.ERROR_UNKNOWN,
+                        "minimum random generated password strength exceeds maximum possible"
+                ));
+            }
         }
     }
 }

+ 2 - 2
src/main/java/password/pwm/util/cli/MainClass.java

@@ -27,12 +27,12 @@ import org.apache.log4j.EnhancedPatternLayout;
 import org.apache.log4j.Layout;
 import org.apache.log4j.Logger;
 import org.apache.log4j.varia.NullAppender;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmEnvironment;
 import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -384,7 +384,7 @@ public class MainClass {
             throws Exception
     {
         final File databaseDirectory;
-        final String pwmDBLocationSetting = config.readSettingAsString(PwmSetting.PWMDB_LOCATION);
+        final String pwmDBLocationSetting = config.readAppProperty(AppProperty.LOCALDB_LOCATION);
         databaseDirectory = FileSystemUtility.figureFilepath(pwmDBLocationSetting, applicationPath);
         return LocalDBFactory.getInstance(databaseDirectory, readonly, null, config);
     }

+ 19 - 10
src/main/java/password/pwm/util/db/DatabaseAccessorImpl.java

@@ -94,14 +94,18 @@ class DatabaseAccessorImpl implements DatabaseAccessor {
                 processSqlException(debugInfo, e);
             }
 
-            final String sqlText;
-            if (!exists) {
-                sqlText = "INSERT INTO " + table.toString() + "(" + DatabaseService.KEY_COLUMN + ", " + DatabaseService.VALUE_COLUMN + ") VALUES(?,?)";
+            if (exists) {
+                final String sqlText = "UPDATE " + table.toString()
+                        + " SET " + DatabaseService.VALUE_COLUMN + "=? WHERE "
+                        + DatabaseService.KEY_COLUMN + "=?";
+                executeUpdate(sqlText, debugInfo, value, key); // note the value/key are reversed for this statement
             } else {
-                sqlText = "UPDATE " + table.toString() + " SET " + DatabaseService.VALUE_COLUMN + "=? WHERE " + DatabaseService.KEY_COLUMN + "=?";
+                final String sqlText = "INSERT INTO " + table.toString()
+                        + "(" + DatabaseService.KEY_COLUMN + ", "
+                        + DatabaseService.VALUE_COLUMN + ") VALUES(?,?)";
+                executeUpdate(sqlText, debugInfo, key, value);
             }
 
-            executeUpdate(sqlText, debugInfo, key, value);
             return !exists;
         });
     }
@@ -223,7 +227,6 @@ class DatabaseAccessorImpl implements DatabaseAccessor {
         return execute(debugInfo, () -> {
             final String sqlStatement = "SELECT COUNT(" + DatabaseService.KEY_COLUMN + ") FROM " + table.name();
 
-
             try (PreparedStatement statement = connection.prepareStatement(sqlStatement)) {
                 try (ResultSet resultSet = statement.executeQuery()) {
                     if (resultSet.next()) {
@@ -404,20 +407,26 @@ class DatabaseAccessorImpl implements DatabaseAccessor {
 
     private boolean containsImpl(final DatabaseTable table, final String key) throws SQLException
     {
-        final String selectSql = "SELECT * FROM " + table.name() + " WHERE " + DatabaseService.KEY_COLUMN + " = ?";
-        try (PreparedStatement selectStatement = connection.prepareStatement(selectSql);) {
+        final String sqlStatement = "SELECT COUNT(" + DatabaseService.KEY_COLUMN + ") FROM " + table.name()
+                + " WHERE " + DatabaseService.KEY_COLUMN + " = ?";
+
+        try (PreparedStatement selectStatement = connection.prepareStatement(sqlStatement);) {
             selectStatement.setString(1, key);
             selectStatement.setMaxRows(1);
 
             try (ResultSet resultSet = selectStatement.executeQuery()) {
-                return resultSet.next();
+                if (resultSet.next()) {
+                    return resultSet.getInt(1) > 0;
+                }
             }
         }
+
+        return false;
     }
 
     private void executeUpdate(final String sqlStatement, final DatabaseUtil.DebugInfo debugInfo, final String... params) throws DatabaseException
     {
-        try (PreparedStatement statement = connection.prepareStatement(sqlStatement)){
+        try (PreparedStatement statement = connection.prepareStatement(sqlStatement) ){
             for (int i = 0; i < params.length; i++) {
                 statement.setString(i + 1, params[i]);
             }

+ 1 - 0
src/main/java/password/pwm/util/db/DatabaseTable.java

@@ -30,4 +30,5 @@ public enum DatabaseTable {
     TOKENS,
     OTP,
     PW_NOTIFY,
+    CLUSTER_STATE,
 }

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

@@ -57,6 +57,7 @@ import java.util.TimeZone;
 import java.util.TreeSet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
@@ -348,7 +349,7 @@ public class JavaHelper {
         return new CSVPrinter(new OutputStreamWriter(outputStream,PwmConstants.DEFAULT_CHARSET), PwmConstants.DEFAULT_CSV_FORMAT);
     }
 
-    public static ExecutorService makeSingleThreadExecutorService(
+    public static ScheduledExecutorService makeSingleThreadExecutorService(
             final PwmApplication pwmApplication,
             final Class clazz
     )

+ 3 - 3
src/main/java/password/pwm/util/java/TimeDuration.java

@@ -52,7 +52,7 @@ public class TimeDuration implements Comparable, Serializable {
     public static final TimeDuration ZERO = new TimeDuration(0);
     public static final TimeDuration MILLISECOND = new TimeDuration(1, TimeUnit.MILLISECONDS);
     public static final TimeDuration SECOND = new TimeDuration(1, TimeUnit.SECONDS);
-    public static final TimeDuration SECONDS_10 = new TimeDuration(30, TimeUnit.SECONDS);
+    public static final TimeDuration SECONDS_10 = new TimeDuration(10, TimeUnit.SECONDS);
     public static final TimeDuration SECONDS_30 = new TimeDuration(30, TimeUnit.SECONDS);
     public static final TimeDuration MINUTE = new TimeDuration(1, TimeUnit.MINUTES);
     public static final TimeDuration HOUR = new TimeDuration(1, TimeUnit.HOURS);
@@ -92,8 +92,8 @@ public class TimeDuration implements Comparable, Serializable {
         return new TimeDuration(System.currentTimeMillis(), instant.toEpochMilli());
     }
 
-    public static String compactFromCurrent(final long ms) {
-        return new TimeDuration(System.currentTimeMillis(), ms).asCompactString();
+    public static String compactFromCurrent(final Instant instant) {
+        return TimeDuration.fromCurrent(instant).asCompactString();
     }
 
     public static String asCompactString(final long ms) {

+ 3 - 2
src/main/java/password/pwm/util/localdb/Derby_LocalDB.java

@@ -35,6 +35,7 @@ import java.sql.Connection;
 import java.sql.Driver;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Properties;
 
@@ -142,7 +143,7 @@ public class Derby_LocalDB extends AbstractJDBC_LocalDB {
     }
 
     private void reclaimAllSpace(final Connection dbConnection) {
-        final java.util.Date startTime = new java.util.Date();
+        final Instant startTime = Instant.now();
         final long startSize = FileSystemUtility.getFileDirectorySize(dbDirectory);
         LOGGER.debug("beginning reclaim space in all tables startSize=" + StringUtil.formatDiskSize(startSize));
         for (final LocalDB.DB db : LocalDB.DB.values()) {
@@ -150,7 +151,7 @@ public class Derby_LocalDB extends AbstractJDBC_LocalDB {
         }
         final long completeSize = FileSystemUtility.getFileDirectorySize(dbDirectory);
         final long sizeDifference = startSize - completeSize;
-        LOGGER.debug("completed reclaim space in all tables; duration=" + TimeDuration.fromCurrent(startTime).asCompactString()
+        LOGGER.debug("completed reclaim space in all tables; duration=" + TimeDuration.compactFromCurrent(startTime)
                 + ", startSize=" + StringUtil.formatDiskSize(startSize)
                 + ", completeSize=" + StringUtil.formatDiskSize(completeSize)
                 + ", sizeDifference=" + StringUtil.formatDiskSize(sizeDifference)

+ 8 - 40
src/main/java/password/pwm/ws/server/RestRequestBean.java

@@ -22,56 +22,24 @@
 
 package password.pwm.ws.server;
 
+import lombok.Getter;
+import lombok.Setter;
 import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.option.WebServiceUsage;
 import password.pwm.http.PwmSession;
 
 import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
 
+@Getter
+@Setter
 public class RestRequestBean implements Serializable {
     private boolean authenticated;
     private boolean external;
     private UserIdentity userIdentity;
     private PwmSession pwmSession;
     private PwmApplication pwmApplication;
-
-    public boolean isAuthenticated() {
-        return authenticated;
-    }
-
-    public void setAuthenticated(final boolean authenticated) {
-        this.authenticated = authenticated;
-    }
-
-    public boolean isExternal() {
-        return external;
-    }
-
-    public void setExternal(final boolean external) {
-        this.external = external;
-    }
-
-    public UserIdentity getUserIdentity() {
-        return userIdentity;
-    }
-
-    public void setUserIdentity(final UserIdentity userIdentity) {
-        this.userIdentity = userIdentity;
-    }
-
-    public PwmSession getPwmSession() {
-        return pwmSession;
-    }
-
-    public void setPwmSession(final PwmSession pwmSession) {
-        this.pwmSession = pwmSession;
-    }
-
-    public PwmApplication getPwmApplication() {
-        return pwmApplication;
-    }
-
-    public void setPwmApplication(final PwmApplication pwmApplication) {
-        this.pwmApplication = pwmApplication;
-    }
+    private final Set<WebServiceUsage> webServiceUsages = new HashSet<>();
 }

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

@@ -106,7 +106,7 @@ public class RestResultBean implements Serializable {
         final RestResultBean restResultBean = new RestResultBean();
         restResultBean.setError(true);
         restResultBean.setErrorMessage(errorInformation.toUserStr(locale, config));
-        if (forceDetail || pwmApplication.determineIfDetailErrorMsgShown()) {
+        if (forceDetail || (pwmApplication != null && pwmApplication.determineIfDetailErrorMsgShown())) {
             restResultBean.setErrorDetail(errorInformation.toDebugStr());
         }
         restResultBean.setErrorCode(errorInformation.getError().getErrorCode());

+ 7 - 20
src/main/java/password/pwm/ws/server/RestServerHelper.java

@@ -23,7 +23,6 @@
 package password.pwm.ws.server;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
-import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
@@ -35,20 +34,19 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.filter.AuthenticationFilter;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.util.LocaleHelper;
-import password.pwm.util.PasswordData;
 import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Locale;
@@ -56,9 +54,12 @@ import java.util.Locale;
 public abstract class RestServerHelper {
     private static final PwmLogger LOGGER = PwmLogger.forClass(RestServerHelper.class);
 
-    public static javax.ws.rs.core.Response doHtmlRedirect() throws URISyntaxException {
-        final URI uri = javax.ws.rs.core.UriBuilder.fromUri("../reference/rest.jsp?forwardedFromRestServer=true").build();
-        return javax.ws.rs.core.Response.temporaryRedirect(uri).build();
+    public static javax.ws.rs.core.Response handleHtmlRequest() throws URISyntaxException {
+        final String msg = "This REST service requires an appropriate "
+                + HttpHeader.Accept.getHttpName() + " header to be set.  This request used an html "
+                + HttpHeader.Accept.getHttpName() + " header, which is not supported.  See documentation at \"/public/reference/\".";
+        final String content = "<html><body>" + msg + "</body></html>";
+        return javax.ws.rs.core.Response.status(Response.Status.NOT_ACCEPTABLE).entity(content).build();
     }
 
     public static RestRequestBean initializeRestRequest(
@@ -126,20 +127,6 @@ public abstract class RestServerHelper {
                     throw new PwmUnrecoverableException(errorInformation);
                 }
             }
-
-            final PasswordData secretKey = pwmApplication.getConfig().readSettingAsPassword(PwmSetting.WEBSERVICES_EXTERNAL_SECRET);
-            if (secretKey != null) {
-                final String headerName = pwmApplication.getConfig().readAppProperty(AppProperty.SECURITY_WS_REST_SERVER_SECRET_HEADER);
-                final String headerValue = pwmRequest.readHeaderValueAsString(headerName);
-                if (headerValue == null || headerValue.isEmpty()) {
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, "request is missing security header " + headerName);
-                    throw new PwmUnrecoverableException(errorInformation);
-                }
-                if (!headerValue.equals(secretKey.getStringValue())) {
-                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, "authenticated user does not have correct security header");
-                    throw new PwmUnrecoverableException(errorInformation);
-                }
-            }
         }
 
         final boolean adminPermission= restRequestBean.getPwmSession().getSessionManager().checkPermission(pwmApplication, Permission.PWMADMIN);

+ 89 - 0
src/main/java/password/pwm/ws/server/StandaloneRestHelper.java

@@ -0,0 +1,89 @@
+/*
+ * 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
+ */
+
+package password.pwm.ws.server;
+
+import com.novell.ldapchai.util.StringHelper;
+import password.pwm.PwmApplication;
+import password.pwm.config.NamedSecretData;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.WebServiceUsage;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ContextManager;
+import password.pwm.util.BasicAuthInfo;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class StandaloneRestHelper {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(StandaloneRestHelper.class);
+
+    public static StandaloneRestRequestBean initialize(final HttpServletRequest httpServletRequest)
+            throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = ContextManager.getPwmApplication(httpServletRequest.getServletContext());
+
+        final Set<WebServiceUsage> usages = readWebServiceSecretAuthorizations(pwmApplication, httpServletRequest);
+
+        return StandaloneRestRequestBean.builder()
+                .pwmApplication(pwmApplication)
+                .authorizedUsages(Collections.unmodifiableSet(usages))
+                .build();
+    }
+
+    private static Set<WebServiceUsage> readWebServiceSecretAuthorizations(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest httpServletRequest
+    )
+    {
+        final Set<WebServiceUsage> usages = new HashSet<>();
+
+        final BasicAuthInfo basicAuthInfo = BasicAuthInfo.parseAuthHeader(pwmApplication, httpServletRequest);
+        if (basicAuthInfo != null) {
+            final Map<String, NamedSecretData> secrets = pwmApplication.getConfig().readSettingAsNamedPasswords(PwmSetting.WEBSERVICES_EXTERNAL_SECRET);
+            final NamedSecretData namedSecretData = secrets.get(basicAuthInfo.getUsername());
+            if (namedSecretData != null) {
+                if (namedSecretData.getPassword().equals(basicAuthInfo.getPassword())) {
+                    final List<WebServiceUsage> namedSecrets = JavaHelper.readEnumListFromStringCollection(WebServiceUsage.class, namedSecretData.getUsage());
+                    usages.addAll(namedSecrets);
+                    LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                            + basicAuthInfo.getUsername() + "\"), granting usage access to "
+                            + StringHelper.stringCollectionToString(namedSecretData.getUsage(), ","));
+                } else {
+                    LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                            + basicAuthInfo.getUsername() + "\") with an incorrect password");
+                }
+            } else {
+                LOGGER.trace("REST request to " + httpServletRequest.getRequestURI() + " specified a basic auth username (\""
+                        + basicAuthInfo.getUsername() + "\") that does not correspond to a configured web secret");
+            }
+        }
+
+        return Collections.unmodifiableSet(usages);
+    }
+}

+ 37 - 0
src/main/java/password/pwm/ws/server/StandaloneRestRequestBean.java

@@ -0,0 +1,37 @@
+/*
+ * 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
+ */
+
+package password.pwm.ws.server;
+
+import lombok.Builder;
+import lombok.Getter;
+import password.pwm.PwmApplication;
+import password.pwm.config.option.WebServiceUsage;
+
+import java.util.Set;
+
+@Getter
+@Builder
+public class StandaloneRestRequestBean {
+    private final Set<WebServiceUsage> authorizedUsages;
+    private final PwmApplication pwmApplication;
+}

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestAppDataServer.java

@@ -106,7 +106,7 @@ public class RestAppDataServer extends AbstractRestServer {
     @GET
     @Produces(MediaType.TEXT_HTML)
     public javax.ws.rs.core.Response doHtmlRedirect() throws URISyntaxException {
-        return RestServerHelper.doHtmlRedirect();
+        return RestServerHelper.handleHtmlRequest();
     }
 
     @GET

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestChallengesServer.java

@@ -144,7 +144,7 @@ public class RestChallengesServer extends AbstractRestServer {
     @GET
     @Produces(MediaType.TEXT_HTML)
     public javax.ws.rs.core.Response doHtmlRedirect() throws URISyntaxException {
-        return RestServerHelper.doHtmlRedirect();
+        return RestServerHelper.handleHtmlRequest();
     }
 
     @GET

+ 10 - 8
src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java

@@ -203,22 +203,23 @@ public class RestRandomPasswordServer extends AbstractRestServer {
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
-        final RandomPasswordGenerator.RandomGeneratorConfig randomConfig = new RandomPasswordGenerator.RandomGeneratorConfig();
+        final RandomPasswordGenerator.RandomGeneratorConfig.RandomGeneratorConfigBuilder randomConfigBuilder
+                = RandomPasswordGenerator.RandomGeneratorConfig.builder();
         if (jsonInput.strength > 0 && jsonInput.strength <= 100) {
-            randomConfig.setMinimumStrength(jsonInput.strength);
+            randomConfigBuilder.minimumStrength(jsonInput.strength);
         }
         if (jsonInput.minLength > 0 && jsonInput.minLength <= 100 * 1024) {
-            randomConfig.setMinimumLength(jsonInput.minLength);
+            randomConfigBuilder.minimumLength(jsonInput.minLength);
         }
         if (jsonInput.maxLength > 0 && jsonInput.maxLength <= 100 * 1024) {
-            randomConfig.setMaximumLength(jsonInput.maxLength);
+            randomConfigBuilder.maximumLength(jsonInput.maxLength);
         }
         if (jsonInput.chars != null) {
             final List<String> charValues = new ArrayList<>();
             for (int i = 0; i < jsonInput.chars.length(); i++) {
                 charValues.add(String.valueOf(jsonInput.chars.charAt(i)));
             }
-            randomConfig.setSeedlistPhrases(charValues);
+            randomConfigBuilder.seedlistPhrases(charValues);
         }
 
         if (!jsonInput.noUser && restRequestBean.getPwmSession().isAuthenticated()) {
@@ -231,22 +232,23 @@ public class RestRandomPasswordServer extends AbstractRestServer {
                         ? restRequestBean.getPwmApplication().getProxiedChaiUser(restRequestBean.getUserIdentity())
                         : restRequestBean.getPwmSession().getSessionManager().getActor(restRequestBean.getPwmApplication(),userIdentity);
 
-                randomConfig.setPasswordPolicy(PasswordUtility.readPasswordPolicyForUser(
+                randomConfigBuilder.passwordPolicy(PasswordUtility.readPasswordPolicyForUser(
                         restRequestBean.getPwmApplication(),
                         restRequestBean.getPwmSession().getLabel(),
                         restRequestBean.getUserIdentity(),
                         theUser,
                         restRequestBean.getPwmSession().getSessionStateBean().getLocale()));
             } else {
-                randomConfig.setPasswordPolicy(restRequestBean.getPwmSession().getUserInfo().getPasswordPolicy());
+                randomConfigBuilder.passwordPolicy(restRequestBean.getPwmSession().getUserInfo().getPasswordPolicy());
             }
         } else {
             final Configuration config  = restRequestBean.getPwmApplication().getConfig();
-            randomConfig.setPasswordPolicy(config.getPasswordPolicy(
+            randomConfigBuilder.passwordPolicy(config.getPasswordPolicy(
                     config.getPasswordProfileIDs().iterator().next(),
                     restRequestBean.getPwmSession().getSessionStateBean().getLocale()));
         }
 
+        final RandomPasswordGenerator.RandomGeneratorConfig randomConfig = randomConfigBuilder.build();
         final PasswordData randomPassword = RandomPasswordGenerator.createRandomPassword(restRequestBean.getPwmSession().getLabel(), randomConfig, restRequestBean.getPwmApplication());
         final JsonOutput outputMap = new JsonOutput();
         outputMap.password = randomPassword.getStringValue();

+ 15 - 14
src/main/java/password/pwm/ws/server/rest/RestSigningServer.java

@@ -24,16 +24,17 @@ package password.pwm.ws.server.rest;
 
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
+import password.pwm.config.option.WebServiceUsage;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.secure.SecureService;
-import password.pwm.ws.server.RestRequestBean;
 import password.pwm.ws.server.RestResultBean;
-import password.pwm.ws.server.RestServerHelper;
-import password.pwm.ws.server.ServicePermissions;
+import password.pwm.ws.server.StandaloneRestHelper;
+import password.pwm.ws.server.StandaloneRestRequestBean;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.POST;
@@ -48,12 +49,6 @@ import java.util.concurrent.TimeUnit;
 
 @Path("/signing")
 public class RestSigningServer extends AbstractRestServer {
-    private static final ServicePermissions SERVICE_PERMISSIONS = ServicePermissions.builder()
-            .adminOnly(false)
-            .authRequired(false)
-            .blockExternal(false)
-            .build();
-
 
     @POST
     @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@@ -65,13 +60,19 @@ public class RestSigningServer extends AbstractRestServer {
     )
             throws PwmUnrecoverableException
     {
-        final RestRequestBean restRequestBean;
+        final StandaloneRestRequestBean restRequestBean;
         try {
-            restRequestBean = RestServerHelper.initializeRestRequest(request, response, SERVICE_PERMISSIONS, null);
+            restRequestBean = StandaloneRestHelper.initialize(request);
         } catch (PwmUnrecoverableException e) {
             return RestResultBean.fromError(e.getErrorInformation()).asJsonResponse();
         }
 
+        if (!restRequestBean.getAuthorizedUsages().contains(WebServiceUsage.SigningForm)) {
+            final String errorMsg = "request is not authenticated with permission for SigningForm";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg);
+            return RestResultBean.fromError(errorInformation).asJsonResponse();
+        }
+
         try {
             if (inputFormData != null) {
                 final SecureService securityService = restRequestBean.getPwmApplication().getSecureService();
@@ -92,11 +93,12 @@ public class RestSigningServer extends AbstractRestServer {
 
     public static Map<String,String> readSignedFormValue(final PwmApplication pwmApplication, final String input) throws PwmUnrecoverableException
     {
-        final TimeDuration MAX_FORM_AGE = new TimeDuration(5, TimeUnit.MINUTES);
+        final Integer maxAgeSeconds = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.WS_REST_SERVER_SIGNING_FORM_TIMEOUT_SECONDS));
+        final TimeDuration maxAge = new TimeDuration(maxAgeSeconds, TimeUnit.SECONDS);
         final SignedFormData signedFormData = pwmApplication.getSecureService().decryptObject(input, SignedFormData.class);
         if (signedFormData != null) {
             if (signedFormData.getTimestamp() != null) {
-                if (TimeDuration.fromCurrent(signedFormData.getTimestamp()).isLongerThan(MAX_FORM_AGE)) {
+                if (TimeDuration.fromCurrent(signedFormData.getTimestamp()).isLongerThan(maxAge)) {
                     throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_SECURITY_VIOLATION,"signedForm data is too old"));
                 }
 
@@ -112,5 +114,4 @@ public class RestSigningServer extends AbstractRestServer {
         private Instant timestamp;
         private Map<String,String> formData;
     }
-
 }

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestStatusServer.java

@@ -57,7 +57,7 @@ public class RestStatusServer extends AbstractRestServer {
     @GET
     @Produces(MediaType.TEXT_HTML)
     public javax.ws.rs.core.Response doHtmlRedirect() throws URISyntaxException {
-        return RestServerHelper.doHtmlRedirect();
+        return RestServerHelper.handleHtmlRequest();
     }
 
     @GET

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java

@@ -61,7 +61,7 @@ public class RestVerifyOtpServer extends AbstractRestServer {
     @GET
     @Produces(MediaType.TEXT_HTML)
     public Response doHtmlRedirect() throws URISyntaxException {
-        return RestServerHelper.doHtmlRedirect();
+        return RestServerHelper.handleHtmlRequest();
     }
 
     @POST

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestVerifyResponsesServer.java

@@ -90,7 +90,7 @@ public class RestVerifyResponsesServer extends AbstractRestServer {
     @GET
     @Produces(MediaType.TEXT_HTML)
     public Response doHtmlRedirect() throws URISyntaxException {
-        return RestServerHelper.doHtmlRedirect();
+        return RestServerHelper.handleHtmlRequest();
     }
 
     @POST

+ 7 - 1
src/main/resources/password/pwm/AppProperty.properties

@@ -48,6 +48,10 @@ client.warningHeader.show=true
 client.pwShowRevertTimeout=45000
 client.js.enableHtml5Dialog=true
 client.jsp.showIcons=true
+cluster.db.enable=true
+cluster.db.heartbeatSeconds=60
+cluster.db.nodeTimeoutSeconds=600
+cluster.db.nodePurgeSeconds=86400
 config.reloadOnChange=true
 config.maxJdbcJarSize=10240000
 config.maxPersistentLoginSeconds=3600
@@ -176,6 +180,7 @@ ldap.oracle.postTempPasswordUseCurrentTime=false
 localdb.aggressiveCompact.enabled=false
 localdb.implementation=password.pwm.util.localdb.Xodus_LocalDB
 localdb.initParameters=
+localdb.location=LocalDB
 localdb.logWriter.bufferSize=500
 localdb.logWriter.maxBufferWaitMs=60000
 localdb.logWriter.maxTrimSize=5001
@@ -248,7 +253,7 @@ security.input.trim=true
 security.input.password.trim=false
 security.input.themeMatchRegex=^[0-9a-zA-Z-_]*$
 security.ws.rest.clientKeyLength=32
-security.ws.rest.server.secretKeyHeader=RestSecretKey
+security.ws.rest.server.secretKeyHeader=AuthorizationSecret
 security.sharedHistory.hashIterations=100000
 security.sharedHistory.hashName=SHA-512
 security.sharedHistory.caseInsensitive=true
@@ -269,4 +274,5 @@ wordlist.builtin.path=/WEB-INF/wordlist.zip
 wordlist.maxCharLength=64
 wordlist.minCharLength=2
 ws.restClient.pwRule.haltOnError=true
+ws.restServer.signing.form.timeoutSeconds=120
 password.policy.allowMacroInRegexSetting=true

+ 56 - 9
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -121,6 +121,7 @@
     <setting hidden="false" key="knownLocales" level="2">
         <default>
             <value><![CDATA[en::us]]></value>
+            <value><![CDATA[en_CA::ca]]></value>
             <value><![CDATA[ca::catalonia]]></value>
             <value><![CDATA[cs::cz]]></value>
             <value><![CDATA[da::dk]]></value>
@@ -129,6 +130,7 @@
             <value><![CDATA[es::es]]></value>
             <value><![CDATA[fi::fi]]></value>
             <value><![CDATA[fr::fr]]></value>
+            <value><![CDATA[fr_CA::ca]]></value>
             <value><![CDATA[hu::hu]]></value>
             <value><![CDATA[iw::il]]></value>
             <value><![CDATA[it::it]]></value>
@@ -2008,6 +2010,38 @@
             <value locale="es"><![CDATA[{"text":"¿Cuál es la película que menos le gusta?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="es"><![CDATA[{"text":"¿Quién era el profesor que menos le gustaba?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="es"><![CDATA[{"text":"¿Cuál es la comida que menos le gusta?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is the name of the main character in your favorite book?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is the name of your favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is the name of your favorite pet?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What was the name of your childhood best friend?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What was your favorite show as a child?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"Who is your favorite author?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is your favorite food?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is your partner's nickname?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is your favorite team?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What street did you grow up on?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What city / town were you born in?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is your favorite vehicle?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"If you could meet someone from history, who would it be?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What is your least favorite film of all time?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"Who was your least favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="en_CA"><![CDATA[{"text":"What food do you dislike the most?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is the name of the main character in your favorite book?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is the name of your favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is the name of your favorite pet?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What was the name of your childhood best friend?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What was your favorite show as a child?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"Who is your favorite author?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is your favorite food?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is your partner's nickname?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is your favorite team?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What street did you grow up on?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What city / town were you born in?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is your favorite vehicle?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"If you could meet someone from history, who would it be?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What is your least favorite film of all time?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"Who was your least favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="fr_CA"><![CDATA[{"text":"What food do you dislike the most?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="fr"><![CDATA[{"text":"Comment s'appelle le héros de votre livre préféré ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="fr"><![CDATA[{"text":"Comment s'appelle votre professeur préféré ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="fr"><![CDATA[{"text":"Quel est le nom de votre animal préféré ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
@@ -2024,6 +2058,22 @@
             <value locale="fr"><![CDATA[{"text":"Quel film avez-vous toujours détesté ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="fr"><![CDATA[{"text":"Qui est le professeur que vous avez le plus détesté ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="fr"><![CDATA[{"text":"Quel plat détestez-vous le plus ?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is the name of the main character in your favorite book?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is the name of your favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is the name of your favorite pet?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What was the name of your childhood best friend?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What was your favorite show as a child?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"Who is your favorite author?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is your favorite food?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is your partner's nickname?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is your favorite team?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What street did you grow up on?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What city / town were you born in?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is your favorite vehicle?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"If you could meet someone from history, who would it be?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What is your least favorite film of all time?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"Who was your least favorite teacher?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
+            <value locale="iw"><![CDATA[{"text":"What food do you dislike the most?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="it"><![CDATA[{"text":"Come si chiama il protagonista del tuo libro preferito?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="it"><![CDATA[{"text":"Come si chiama il tuo insegnante preferito?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
             <value locale="it"><![CDATA[{"text":"Come si chiama il tuo animale domestico preferito?","minLength":4,"maxLength":200,"adminDefined":true,enforceWordlist:true,maxQuestionCharsInAnswer:3}]]></value>
@@ -2527,6 +2577,7 @@
         <ldapPermission actor="proxy" access="write"/>
         <flag>Form_ShowUniqueOption</flag>
         <flag>Form_ShowRequiredOption</flag>
+        <flag>Form_ShowReadOnlyOption</flag>
         <default>
             <value>{"name":"mail","minimumLength":1,"maximumLength":64,"type":"email","required":true,"confirmationRequired":false,"readonly":false,"unique":true,"labels":{"":"Email Address"},"regexErrors":{"":"Email Address has invalid characters"},"description":{"":""},"placeholder":"username@example.com","selectOptions":{},regex:"^[a-zA-Z0-9 .,'@]*$"}</value>
             <value>{"name":"givenName","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"First Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{},regex:"^[a-zA-Z0-9 .,'@]*$"}</value>
@@ -3562,10 +3613,11 @@
             <value>false</value>
         </default>
     </setting>
-    <setting hidden="false" key="webservices.external.secret" level="2">
-        <default>
-            <value/>
-        </default>
+    <setting hidden="false" key="webservices.external.secrets" level="2">
+        <default/>
+        <options>
+            <option value="SigningForm">Signing Form Service - /signing/form</option>
+        </options>
     </setting>
     <setting hidden="false" key="webservices.queryMatch" level="2">
         <ldapPermission actor="proxy" access="read"/>
@@ -3677,11 +3729,6 @@
             <value>false</value>
         </default>
     </setting>
-    <setting hidden="true" key="pwmDb.location" level="2" required="true">
-        <default>
-            <value><![CDATA[LocalDB]]></value>
-        </default>
-    </setting>
     <!-- DEPRECATED SETTINGS -->
     <category hidden="false" key="TEMPLATES">
     </category>

+ 1 - 1
src/main/resources/password/pwm/i18n/Admin.properties

@@ -241,7 +241,7 @@ Statistic_Description.HelpdeskTokenSent=Number of Help Desk verification tokens
 Statistic_Label.HelpdeskUnlock=Help Desk Unlocks
 Statistic_Description.HelpdeskUnlock=Number of Help Desk unlock user events.
 Statistic_Label.HelpdeskVerifyOTP=Help Desk OTP Verifications
-Statistic_Description.HelpdeskVerifyOTP=Number of successful Help Desk OPT verification user events.
+Statistic_Description.HelpdeskVerifyOTP=Number of successful Help Desk OTP verification user events.
 Statistic_Label.RestStatus=WebService Status Calls
 Statistic_Description.RestStatus=Number of external web service calls to the /status REST interface.
 Statistic_Label.RestCheckPassword=WebService Check Password Calls

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_ca.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Mostra
 Button_Show_Responses=Mostra les respostes
 Button_Skip=Omet
 Button_SMS=SMS
+Button_TokenResend=Torna a enviar el codi
 Button_Unlock=Desbloqueja
 Button_UnlockPassword=Desbloqueja la contrasenya
 Button_Update=Actualitza
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>A l'iPhone, toqui la icona de l'App Store.</b><
 Display_SetupOtp_Other_Title=Altres
 Display_SetupOtp_Other_Steps=<b>Trobi una aplicaci\u00f3 compatible de dos factors.</b><ul><li>Intenti cercar a la botiga d'aplicacions del seu dispositiu <b>Google Authenticator</b>.<br/>Molts dispositius tenen aplicacions compatibles.</li><li>Intenti buscar una aplicaci\u00f3 que admeti "<b>testimonis de seguretat TOTP"</b> o "RFC6238"</li><li>Baixi i instal\u00b7li l'aplicaci\u00f3.</li></ul><b>Despr\u00e9s, obri i configuri l'aplicaci\u00f3.</b><ol><li>Introdueixi les dades d'aqu\u00ed sota o escanegi el codi segons les instruccions de l'aplicaci\u00f3.</li><li>Quan hagi configurat l'aplicaci\u00f3, cliqui el bot\u00f3 Continua.</li></ol>
 Display_TokenDestination=Destinaci\u00f3 del testimoni
+Display_TokenResend=El vostre codi de seguretat hauria d'arribar aviat. Si ja ha passat una estona i encara no heu rebut cap codi, feu clic al bot\u00f3 Torna a enviar el codi per rebre'n un de nou.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Ja heu inscrit el dispositiu el dia <span class\="timestamp">%1%</span>. Podeu provar el vostre dispositiu actual escrivint el codi generat aqu\u00ed sota. Si continueu, podeu tornar a configurar el dispositiu actual.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Preguntes i respostes secretes
 Field_VerificationMethodAttributes=Dades personals
 Field_VerificationMethodRemoteResponses=Respostes externes
 Field_VerificationMethodNAAF=Autenticaci\u00f3 avan\u00e7ada
-Field_VerificationMethodOAuth=Autenticaci\u00f3 externa
+Field_VerificationMethodOAuth=Autenticaci\u00f3 d'OAuth externa
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Resposta
 Long_Title_ActivateUser=Activar un compte preconfigurat i establir una contrasenya nova.
 Long_Title_Admin=Funcions administratives

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_da.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Vis
 Button_Show_Responses=Vis svar
 Button_Skip=Spring over
 Button_SMS=Sms
+Button_TokenResend=Send koden igen
 Button_Unlock=L\u00e5s op
 Button_UnlockPassword=Adgangskode til opl\u00e5sning
 Button_Update=Opdater
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Tryk p\u00e5 ikonet App Store p\u00e5 din iPhon
 Display_SetupOtp_Other_Title=Andet
 Display_SetupOtp_Other_Steps=<b>Find en kompatibel app til to faktor-godkendelse.</b><ul><li>Pr\u00f8v at s\u00f8ge efter <b>Google Authenticator</b> i enhedens appbutik.<br/>Mange enheder har kompatible apps.</li><li>Pr\u00f8v at finde en app, der underst\u00f8tter "<b>TOTP-sikkerhedstokens"</b> eller "RFC6238"</li><li>Download og install\u00e9r appen.</li></ul><b>\u00c5bn og konfigurer derefter appen.</b><ol><li>Angiv dataene herunder, eller scan koden iht. vejledningen i appen.</li><li>N\u00e5r du har konfigureret appen, skal du klikke p\u00e5 knappen Forts\u00e6t.</li></ol>
 Display_TokenDestination=Tokendestination
+Display_TokenResend=Du b\u00f8r modtage din sikkerhedskode med det samme. Hvis du har ventet et stykket id og endnu ikke har modtaget koden, skal du klikke p\u00e5 knappen Send igen for at f\u00e5 tilsendt en ny kode.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Du har allerede tilmeldt din enhed p\u00e5 <span class\="timestamp">%1%</span>. Du kan teste din aktuelle enhed ved at skrive den genererede kode herunder. Hvis du forts\u00e6tter, kan du konfigurere din aktuelle enhed igen.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Hemmelige sp\u00f8rgsm\u00e5l og svar
 Field_VerificationMethodAttributes=Personlige data
 Field_VerificationMethodRemoteResponses=Eksterne svar
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Ekstern godkendelse
+Field_VerificationMethodOAuth=Ekstern OAuth-godkendelse
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Svar
 Long_Title_ActivateUser=Aktiv\u00e9r en forudkonfigureret konto, og angiv en ny adgangskode.
 Long_Title_Admin=Administrative funktioner

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_de.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Anzeigen
 Button_Show_Responses=Antworten anzeigen
 Button_Skip=\u00dcberspringen
 Button_SMS=SMS
+Button_TokenResend=Code neu senden
 Button_Unlock=Entsperren
 Button_UnlockPassword=Passwort entsperren
 Button_Update=Aktualisieren
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Tippen Sie auf dem iPhone auf das App Store-Sym
 Display_SetupOtp_Other_Title=Sonstige
 Display_SetupOtp_Other_Steps=<b>Suchen Sie eine kompatible Zwei-Faktoren-App.</b><ul><li>Suchen Sie im App-Store Ihres Ger\u00e4ts nach <b>Google Authenticator</b>.<br/>F\u00fcr viele Ger\u00e4te sind kompatible Apps verf\u00fcgbar.</li><li>Suchen Sie nach einer App, die "<b>TOTP-Sicherheitstoken"</b> oder "RFC6238" unterst\u00fctzt</li><li> Laden Sie die Anwendung herunter und installieren Sie sie.</li></ul><b>\u00d6ffnen und konfigurieren Sie dann die App.</b><ol><li>Geben Sie die Daten unten ein oder scannen Sie den Code wie von der App angewiesen.</li><li>Klicken Sie nach der Konfiguration der App auf die Schaltfl\u00e4che 'Weiter'.</li></ol>
 Display_TokenDestination=Token-Ziel
+Display_TokenResend=Der Sicherheitscode sollte kurz nach der Anforderung ankommen. Wenn Sie bereits eine gewisse Zeit gewartet haben und keinen Code erhalten haben, klicken Sie auf die Schaltfl\u00e4che 'Code neu senden', damit Ihnen ein neuer Code gesendet wird.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Sie haben das Ger\u00e4t bereits am <span class\="timestamp">%1%</span> registriert. Um das aktuelle Ger\u00e4t zu testen, geben Sie unten den generierten Code ein. Wenn Sie fortfahren, k\u00f6nnen Sie das aktuelle Ger\u00e4t neu konfigurieren.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Sicherheitsfragen und -antworten
 Field_VerificationMethodAttributes=Pers\u00f6nliche Daten
 Field_VerificationMethodRemoteResponses=Externe Antworten
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Externe Authentifizierung
+Field_VerificationMethodOAuth=Externe OAuth-Authentifizierung
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Antwort
 Long_Title_ActivateUser=Vorkonfiguriertes Konto aktivieren und ein neues Passwort erstellen.
 Long_Title_Admin=Verwaltungsfunktionen

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_es.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Mostrar
 Button_Show_Responses=Mostrar respuestas
 Button_Skip=Omitir
 Button_SMS=SMS
+Button_TokenResend=Volver a enviar c\u00f3digo
 Button_Unlock=Desbloquear
 Button_UnlockPassword=Desbloquear contrase\u00f1a
 Button_Update=Actualizar
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>En su iPhone, toque el icono de App Store.</b><
 Display_SetupOtp_Other_Title=Otro
 Display_SetupOtp_Other_Steps=<b>Busque una aplicaci\u00f3n compatible de dos factores.</b><ul><li>Pruebe a buscar en la tienda de aplicaciones de su dispositivo la aplicaci\u00f3n <b>Google Authenticator</b>.<br/>Muchos dispositivos tienen aplicaciones compatibles.</li><li>Trate de buscar una que sea compatible con "<b>testigos de seguridad TOTP"</b> o "RFC6238"</li><li>Descargue e instale la aplicaci\u00f3n.</li></ul>A continuaci\u00f3n, abra y configure la aplicaci\u00f3n.</b><ol><li>Introduzca los datos a continuaci\u00f3n o escanee el c\u00f3digo cuando se lo indique la aplicaci\u00f3n.</li><li>Una vez que haya configurado la aplicaci\u00f3n, haga clic en el bot\u00f3n para continuar.</li></ol><b>
 Display_TokenDestination=Destino del testigo
+Display_TokenResend=Su c\u00f3digo de seguridad debe llegar inmediatamente. Si lleva esperando un rato y a\u00fan no ha recibido un c\u00f3digo, haga clic en el bot\u00f3n Volver a enviar c\u00f3digo para recibir un c\u00f3digo nuevo.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Ya ha inscrito su dispositivo en <span class\="timestamp">%1%</span>. Puede probar su dispositivo actual introduciendo el c\u00f3digo generado a continuaci\u00f3n. Si contin\u00faa, puede volver a configurar su dispositivo actual.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Preguntas y respuestas secretas
 Field_VerificationMethodAttributes=Datos personales
 Field_VerificationMethodRemoteResponses=Respuestas externas
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Autenticaci\u00f3n externa
+Field_VerificationMethodOAuth=Autenticaci\u00f3n OAuth externa
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Respuesta
 Long_Title_ActivateUser=Active una cuenta preconfigurada y establezca una contrase\u00f1a nueva.
 Long_Title_Admin=Funciones administrativas

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_fr.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Afficher
 Button_Show_Responses=Afficher les r\u00e9ponses
 Button_Skip=Ignorer
 Button_SMS=SMS
+Button_TokenResend=Renvoyer un code
 Button_Unlock=D\u00e9verrouiller
 Button_UnlockPassword=Mot de passe de d\u00e9verrouillage
 Button_Update=Mettre \u00e0 jour
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Sur votre iPhone, appuyez sur l'ic\u00f4ne App
 Display_SetupOtp_Other_Title=Autre
 Display_SetupOtp_Other_Steps=<b>Recherchez une application \u00e0 deux facteurs compatible.</b><ul><li>Essayez de trouver <b>Google Authenticator</b> dans l'App Store de votre appareil.<br/>De nombreux p\u00e9riph\u00e9riques ont des applications compatibles.</li><li>Essayez de trouver une application qui prend en charge les "<b>jetons de s\u00e9curit\u00e9 TOTP</b>" ou "RFC6238".</li><li>T\u00e9l\u00e9chargez et installez l'application.</li></ul><b>Ensuite, ouvrez et configurez l'application.</b><ol><li>Entrez les donn\u00e9es ci-dessous ou scannez le code en suivant les instructions de l'application.</li><li>Une fois l'application configur\u00e9e, cliquez sur le bouton Continuer.</li></ol>
 Display_TokenDestination=Destination du jeton
+Display_TokenResend=Vous devriez recevoir imm\u00e9diatement votre code de s\u00e9curit\u00e9. Si vous attendez depuis un moment sans rien recevoir, cliquez sur le bouton Renvoyer pour en recevoir un nouveau.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Vous avez d\u00e9j\u00e0 inscrit votre p\u00e9riph\u00e9rique le <span class\="timestamp">%1%</span>. Vous pouvez tester votre p\u00e9riph\u00e9rique actuel en introduisant le code g\u00e9n\u00e9r\u00e9 ci-dessous. Si vous continuez, vous pouvez reconfigurer votre p\u00e9riph\u00e9rique actuel.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Questions et r\u00e9ponses secr\u00e8
 Field_VerificationMethodAttributes=Donn\u00e9es personnelles
 Field_VerificationMethodRemoteResponses=R\u00e9ponses externes
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Authentification externe
+Field_VerificationMethodOAuth=Authentification OAuth externe
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=R\u00e9ponse
 Long_Title_ActivateUser=Activez un compte pr\u00e9configur\u00e9 et \u00e9tablissez un nouveau mot de passe.
 Long_Title_Admin=Fonctions administratives

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_it.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Mostra
 Button_Show_Responses=Mostra risposte
 Button_Skip=Ignora
 Button_SMS=SMS
+Button_TokenResend=Reinvia codice
 Button_Unlock=Sblocca
 Button_UnlockPassword=Sblocca password
 Button_Update=Aggiorna
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Toccare l'icona di App Store sull'iPhone.</b><o
 Display_SetupOtp_Other_Title=Altri
 Display_SetupOtp_Other_Steps=<b>Cercare un'app compatibile a due fattori.</b><ul><li>Cercare <b>Google Authenticator</b> nell'app store del proprio dispositivo.<br/>Sono disponibili app compatibili per numerosi dispositivi.</li><li>Provare a cercare un'app che supporti i "<b>token di sicurezza TOTP</b>" o "RFC6238"</li><li>Effettuare il download e installare l'applicazione.</li></ul><b>Al termine dell'installazione, aprire e configurare l'app.</b><ol><li>Immettere i dati riportati di seguito o effettuare la scansione del codice a barre seguendo le istruzioni dell'app.</li><li>Dopo aver configurato l'app, fare clic sul pulsante Continua.</li></ol>
 Display_TokenDestination=Destinazione token
+Display_TokenResend=Il codice di sicurezza dovrebbe arrivare immediatamente. Se dopo aver atteso il codice non \u00e8 stato ricevuto, fare clic sul pulsante Reinvia codice per ricevere un nuovo codice.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Il dispositivo \u00e8 gi\u00e0 stato registrato il <span class\="timestamp">%1%</span>. Per eseguire una prova del dispositivo attuale, digitare di seguito il codice generato. Continuando \u00e8 possibile riconfigurare il dispositivo attuale.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Domande e risposte segrete
 Field_VerificationMethodAttributes=Dati personali
 Field_VerificationMethodRemoteResponses=Risposte esterne
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Autenticazione esterna
+Field_VerificationMethodOAuth=Autenticazione OAuth esterna
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Risposta
 Long_Title_ActivateUser=Attivare un account pre-configurato e impostare una nuova password.
 Long_Title_Admin=Funzioni amministrative

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_ja.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=\u8868\u793a
 Button_Show_Responses=\u56de\u7b54\u3092\u8868\u793a\u3059\u308b
 Button_Skip=\u30b9\u30ad\u30c3\u30d7
 Button_SMS=SMS
+Button_TokenResend=\u30b3\u30fc\u30c9\u306e\u518d\u9001\u4fe1
 Button_Unlock=\u30ed\u30c3\u30af\u89e3\u9664
 Button_UnlockPassword=\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u30ed\u30c3\u30af\u89e3\u9664
 Button_Update=\u66f4\u65b0
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>\u3054\u4f7f\u7528\u306eiPhone\u3067\u3001App S
 Display_SetupOtp_Other_Title=\u305d\u306e\u4ed6
 Display_SetupOtp_Other_Steps=<b>\u4e92\u63db\u6027\u306e\u3042\u308b2\u6bb5\u968e\u8a8d\u8a3c\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u898b\u3064\u3051\u307e\u3059\u3002</b><ul><li>\u3054\u4f7f\u7528\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30b9\u30c8\u30a2\u3067<b>Google Authenticator</b>\u3092\u691c\u7d22\u3057\u307e\u3059\u3002<br/>\u591a\u304f\u306e\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3001\u4e92\u63db\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u3059\u3002</li><li>\u300c<b>TOTP\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fc\u30c8\u30fc\u30af\u30f3\u300d</b>\u307e\u305f\u306f\u300cRFC6238\u300d\u3092\u30b5\u30dd\u30fc\u30c8\u3059\u308b\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u63a2\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002</li><li>\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u307e\u3059\u3002</li></ul><b>\u6b21\u306b\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u8a2d\u5b9a\u3057\u307e\u3059\u3002</b><ol><li>\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001\u4e0b\u306e\u30c7\u30fc\u30bf\u3092\u5165\u529b\u3059\u308b\u304b\u3001\u30b3\u30fc\u30c9\u3092\u30b9\u30ad\u30e3\u30f3\u3057\u307e\u3059\u3002</li><li>\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u305f\u3089\u3001\u6b21\u3078\u9032\u3080\u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002</li></ol>
 Display_TokenDestination=\u30c8\u30fc\u30af\u30f3\u306e\u5b9b\u5148
+Display_TokenResend=\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9\u304c\u9593\u3082\u306a\u304f\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u5f85\u3063\u3066\u3082\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u306a\u3044\u5834\u5408\u306f\u3001\uff3b\u30b3\u30fc\u30c9\u306e\u518d\u9001\u4fe1\uff3d\u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u65b0\u3057\u3044\u30b3\u30fc\u30c9\u3092\u304a\u53d7\u3051\u53d6\u308a\u304f\u3060\u3055\u3044\u3002
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=\u3054\u4f7f\u7528\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u3059\u3067\u306b <span class\="timestamp">%1%</span> \u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u751f\u6210\u3055\u308c\u305f\u4e0b\u306e\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u73fe\u5728\u4f7f\u7528\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u30c6\u30b9\u30c8\u3067\u304d\u307e\u3059\u3002\u7d9a\u884c\u3059\u308b\u3068\u3001\u73fe\u5728\u4f7f\u7528\u3057\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u518d\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=\u79d8\u5bc6\u306e\u8cea\u554f\u3068\
 Field_VerificationMethodAttributes=\u500b\u4eba\u30c7\u30fc\u30bf
 Field_VerificationMethodRemoteResponses=\u5916\u90e8\u5fdc\u7b54
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=\u5916\u90e8\u8a8d\u8a3c
+Field_VerificationMethodOAuth=\u5916\u90e8OAuth\u8a8d\u8a3c
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=\u7b54\u3048
 Long_Title_ActivateUser=\u4e8b\u524d\u8a2d\u5b9a\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u6709\u52b9\u306b\u3057\u3066\u3001\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002
 Long_Title_Admin=\u7ba1\u7406\u6a5f\u80fd

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_nl.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Weergeven
 Button_Show_Responses=Antwoorden weergeven
 Button_Skip=Overslaan
 Button_SMS=Sms
+Button_TokenResend=Code opnieuw verzenden
 Button_Unlock=Ontgrendelen
 Button_UnlockPassword=Wachtwoord ontgrendelen
 Button_Update=Bijwerken
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Tik op uw iPhone op het pictogram App Store.</b
 Display_SetupOtp_Other_Title=Overig
 Display_SetupOtp_Other_Steps=<b>Zoek een compatibele tweeledige app.</b><ul><li>Zoek in de App Store op uw apparaat naar <b>Google Authenticator</b>.<br/>Veel apparaten hebben compatibele apps.</li><li>Probeer een app te vinden die "<b>TOTP-beveiligingstokens"</b> of "RFC6238" ondersteunt. </li><li>Download en installeer de toepassing.</li></ul><b>Open de app en configureer deze vervolgens.</b><ol><li>Voer de onderstaande gegevens in of scan de code volgens de instructies van de app.</li><li>Zodra u de app hebt geconfigureerd, klikt u op de knop Doorgaan.</li></ol>
 Display_TokenDestination=Tokenbestemming
+Display_TokenResend=U moet uw beveiligingscode meteen ontvangen. Als u na enige tijd nog geen code hebt ontvangen, klikt u op de knop Opnieuw verzenden om een nieuwe code te ontvangen.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=U hebt uw apparaat al ingeschreven op <span class\="timestamp">%1%</span>. U kunt uw huidige apparaat testen door de gegenereerde code hieronder in te voeren. Als u doorgaat, kunt u uw huidige apparaat opnieuw configureren.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Geheime vragen en antwoorden
 Field_VerificationMethodAttributes=Persoonlijke gegevens
 Field_VerificationMethodRemoteResponses=Externe antwoorden
 Field_VerificationMethodNAAF=Geavanceerde verificatie
-Field_VerificationMethodOAuth=Externe verificatie
+Field_VerificationMethodOAuth=Externe OAuth-verificatie
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Antwoord
 Long_Title_ActivateUser=Activeer een vooraf geconfigureerde account en stel een nieuw wachtwoord in.
 Long_Title_Admin=Beheerfuncties

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_pl.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Poka\u017c
 Button_Show_Responses=Poka\u017c odpowiedzi
 Button_Skip=Pomi\u0144
 Button_SMS=SMS
+Button_TokenResend=Ponownie wy\u015blij kod
 Button_Unlock=Odblokuj
 Button_UnlockPassword=Odblokuj has\u0142o
 Button_Update=Aktualizuj
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Na telefonie iPhone stuknij ikon\u0119 App Stor
 Display_SetupOtp_Other_Title=Inne
 Display_SetupOtp_Other_Steps=<b>Znajd\u017a zgodn\u0105 aplikacj\u0119 do uwierzytelniania dwuczynnikowego.</b><ul><li>Spr\u00f3buj wyszuka\u0107 w sklepie z aplikacjami urz\u0105dzenia aplikacj\u0119 <b>Google Authenticator</b>.<br/>Wiele urz\u0105dze\u0144 ma zgodne aplikacje.</li><li>Spr\u00f3buj poszuka\u0107 aplikacji, kt\u00f3ra obs\u0142uguje <b>\u201etokeny zabezpiecze\u0144 TOTP\u201d</b> lub \u201eRFC6238\u201d.</li><li>Pobierz i zainstaluj aplikacj\u0119.</li></ul><b>Nast\u0119pnie otw\u00f3rz i skonfiguruj aplikacj\u0119</b><ol><li>Wprowad\u017a dane poni\u017cej lub zeskanuj kod, zgodnie z instrukcj\u0105 aplikacji.</li><li>Po skonfigurowaniu aplikacji kliknij przycisk Kontynuuj.</li></ol>
 Display_TokenDestination=Miejsce docelowe tokenu
+Display_TokenResend=Tw\u00f3j kod zabezpiecze\u0144 powinien zosta\u0107 dostarczony od razu. Je\u015bli czekasz od d\u0142u\u017cszego czasu i kod jeszcze nie nadszed\u0142, kliknij przycisk Ponownie wy\u015blij kod, aby otrzyma\u0107 nowy kod.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Urz\u0105dzenie zosta\u0142o ju\u017c zarejestrowane dnia <span class\="timestamp">%1%</span>. Mo\u017cesz przetestowa\u0107 bie\u017c\u0105ce urz\u0105dzenie, wpisuj\u0105c poni\u017cej wygenerowany kod. W przypadku kontynuowania mo\u017cesz skonfigurowa\u0107 ponownie bie\u017c\u0105ce urz\u0105dzenie.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Tajne pytania i odpowiedzi
 Field_VerificationMethodAttributes=Dane osobowe
 Field_VerificationMethodRemoteResponses=Zewn\u0119trzne odpowiedzi
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Uwierzytelnianie zewn\u0119trzne
+Field_VerificationMethodOAuth=Uwierzytelnianie zewn\u0119trzne OAuth
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Odpowied\u017a
 Long_Title_ActivateUser=Aktywuj wst\u0119pnie skonfigurowane konto i ustal nowe has\u0142o.
 Long_Title_Admin=Funkcje administracyjne

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_pt_BR.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Mostrar
 Button_Show_Responses=Mostrar Respostas
 Button_Skip=Ignorar
 Button_SMS=SMS
+Button_TokenResend=Reenviar C\u00f3digo
 Button_Unlock=Desbloquear
 Button_UnlockPassword=Desbloquear Senha
 Button_Update=Atualizar
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>No iPhone, toque no \u00edcone App Store.</b><o
 Display_SetupOtp_Other_Title=Outro
 Display_SetupOtp_Other_Steps=<b>Encontre um aplicativo de dois fatores compat\u00edvel.</b><ul><li>Tente pesquisar na loja de aplicativos do dispositivo o <b>Google Authenticator</b>.<br/>Muitos dispositivos t\u00eam aplicativos compat\u00edveis.</li><li>Tente procurar um aplicativo que suporte "<b>tokens de seguran\u00e7a TOTP"</b> ou "RFC6238"</li><li>Fa\u00e7a download e instale o aplicativo.</li></ul><b>Em seguida, abra e configure o aplicativo.</b><ol><li>Digite os dados abaixo ou leia o c\u00f3digo conforme as instru\u00e7\u00f5es do aplicativo.</li><li>Depois de ter configurado o aplicativo, clique no bot\u00e3o Continuar.</li></ol>
 Display_TokenDestination=Destino do Token
+Display_TokenResend=Seu c\u00f3digo de seguran\u00e7a deve chegar imediatamente. Se tiver aguardado por um longo tempo e ainda n\u00e3o tiver recebido um c\u00f3digo, clique no bot\u00e3o de reenviar c\u00f3digo para receber um novo c\u00f3digo.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Voc\u00ea j\u00e1 registrou seu dispositivo em <span class\="timestamp">%1%</span>. \u00c9 poss\u00edvel testar o dispositivo atual digitando o c\u00f3digo gerado abaixo. Se continuar, voc\u00ea poder\u00e1 reconfigurar o dispositivo atual.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Perguntas e Respostas Secretas
 Field_VerificationMethodAttributes=Dados Pessoais
 Field_VerificationMethodRemoteResponses=Respostas Externas
 Field_VerificationMethodNAAF=Autentica\u00e7\u00e3o Avan\u00e7ada
-Field_VerificationMethodOAuth=Autentica\u00e7\u00e3o Externa
+Field_VerificationMethodOAuth=Autentica\u00e7\u00e3o OAuth Externa
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Resposta
 Long_Title_ActivateUser=Ative uma conta pr\u00e9-configurada e estabele\u00e7a uma nova senha.
 Long_Title_Admin=Fun\u00e7\u00f5es administrativas

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 2 - 0
src/main/resources/password/pwm/i18n/Display_ru.properties


+ 12 - 1
src/main/resources/password/pwm/i18n/Display_sv.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=Visa
 Button_Show_Responses=Visa svar
 Button_Skip=Hoppa \u00f6ver
 Button_SMS=SMS
+Button_TokenResend=Skicka kod p\u00e5 nytt
 Button_Unlock=L\u00e5s upp
 Button_UnlockPassword=L\u00e5s upp l\u00f6senordet
 Button_Update=Uppdatera
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>Tryck p\u00e5 App Store-ikonen p\u00e5 din iPho
 Display_SetupOtp_Other_Title=\u00d6vrigt
 Display_SetupOtp_Other_Steps=<b>Hitta en kompatibel tv\u00e5faktorapp.</b><ul><li>F\u00f6rs\u00f6k att leta efter <b>Google Authenticator</b> i enhetens appbutik.<br/>M\u00e5nga enheter har kompatibla appar.</li><li>F\u00f6rs\u00f6k leta upp en app med st\u00f6d f\u00f6r "<b>TOTP-s\u00e4kerhetstokens"</b> eller "RFC6238"</li><li>H\u00e4mta och installera appen.</li></ul><b>Sedan \u00f6ppnar och konfigurerar du appen.</b><ol><li>Ange data enligt nedan eller skanna koden enligt appens instruktioner.</li><li>N\u00e4r du har konfigurerat appen klickar du p\u00e5 knappen f\u00f6r att forts\u00e4tta.</li></ol>
 Display_TokenDestination=Tokenm\u00e5l
+Display_TokenResend=Din s\u00e4kerhetskod b\u00f6r komma direkt. Klicka p\u00e5 knappen Skicka kod p\u00e5 nytt f\u00f6r att f\u00e5 en ny kod om du har v\u00e4ntat en stund och fortfarande inte f\u00e5tt den.
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=Du har redan registrerat din enhet p\u00e5 <span class\="timestamp">%1%</span>. Du kan testa din nuvarande enhet genom att skriva den genererade koden nedan. Om du forts\u00e4tter kan du konfigurera om din nuvarande enhet.
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=Hemliga fr\u00e5gor och svar
 Field_VerificationMethodAttributes=Personliga data
 Field_VerificationMethodRemoteResponses=Externa svar
 Field_VerificationMethodNAAF=Advanced Authentication
-Field_VerificationMethodOAuth=Extern autentisering
+Field_VerificationMethodOAuth=Extern 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=Aktivera ett f\u00f6rkonfigurerat konto och ange ett nytt l\u00f6senord.
 Long_Title_Admin=Administrativa funktioner

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_zh_CN.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=\u663e\u793a
 Button_Show_Responses=\u663e\u793a\u56de\u7b54
 Button_Skip=\u8df3\u8fc7
 Button_SMS=\u77ed\u4fe1
+Button_TokenResend=\u91cd\u53d1\u9001\u4ee3\u7801
 Button_Unlock=\u89e3\u9664\u9501\u5b9a
 Button_UnlockPassword=\u89e3\u9664\u9501\u5b9a\u53e3\u4ee4
 Button_Update=\u66f4\u65b0
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>\u5728\u60a8\u7684 iPhone \u4e0a\uff0c\u70b9\u5
 Display_SetupOtp_Other_Title=\u5176\u4ed6
 Display_SetupOtp_Other_Steps=<b>\u67e5\u627e\u517c\u5bb9\u7684\u53cc\u91cd\u9274\u5b9a\u5e94\u7528\u7a0b\u5e8f\u3002</b><ul><li>\u5c1d\u8bd5\u5728\u60a8\u7684\u8bbe\u5907\u5e94\u7528\u7a0b\u5e8f\u5546\u5e97\u4e2d\u641c\u7d22 <b>Google Authenticator</b>\u3002<br/>\u5f88\u591a\u8bbe\u5907\u90fd\u6709\u517c\u5bb9\u7684\u5e94\u7528\u7a0b\u5e8f\u3002</li><li>\u5c1d\u8bd5\u67e5\u627e\u652f\u6301\u201c<b>TOTP \u5b89\u5168\u4ee4\u724c</b>\u201d\u6216\u201cRFC6238\u201d\u7684\u5e94\u7528\u7a0b\u5e8f</li><li>\u4e0b\u8f7d\u5e76\u5b89\u88c5\u8be5\u5e94\u7528\u7a0b\u5e8f\u3002</li></ul><b>\u63a5\u4e0b\u6765\uff0c\u6253\u5f00\u5e76\u914d\u7f6e\u8be5\u5e94\u7528\u7a0b\u5e8f\u3002</b><ol><li>\u8f93\u5165\u4ee5\u4e0b\u6570\u636e\u6216\u6309\u5e94\u7528\u7a0b\u5e8f\u8bf4\u660e\u626b\u63cf\u4ee3\u7801\u3002</li><li>\u914d\u7f6e\u4e86\u8be5\u5e94\u7528\u7a0b\u5e8f\u540e\uff0c\u8bf7\u5355\u51fb\u201c\u7ee7\u7eed\u201d\u6309\u94ae\u3002</li></ol>
 Display_TokenDestination=\u4ee4\u724c\u76ee\u6807
+Display_TokenResend=\u60a8\u5c06\u7acb\u5373\u6536\u5230\u5b89\u5168\u4ee3\u7801\u3002\u5982\u679c\u957f\u65f6\u95f4\u672a\u6536\u5230\uff0c\u53ef\u5355\u51fb\u91cd\u53d1\u9001\u4ee3\u7801\u6309\u94ae\u4ee5\u63a5\u6536\u65b0\u4ee3\u7801\u3002
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=\u60a8\u5df2\u5728 <span class\="timestamp">%1%</span> \u6ce8\u518c\u4e86\u8bbe\u5907\u3002\u60a8\u53ef\u4ee5\u901a\u8fc7\u5728\u4e0b\u9762\u952e\u5165\u751f\u6210\u7684\u4ee3\u7801\u6765\u6d4b\u8bd5\u5f53\u524d\u8bbe\u5907\u3002\u5982\u679c\u7ee7\u7eed\uff0c\u60a8\u53ef\u4ee5\u91cd\u65b0\u914d\u7f6e\u5f53\u524d\u8bbe\u5907\u3002
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=\u673a\u5bc6\u95ee\u9898\u548c\u7b54\
 Field_VerificationMethodAttributes=\u4e2a\u4eba\u6570\u636e
 Field_VerificationMethodRemoteResponses=\u5916\u90e8\u56de\u7b54
 Field_VerificationMethodNAAF=\u9ad8\u7ea7\u9274\u5b9a
-Field_VerificationMethodOAuth=\u5916\u90e8\u9274\u5b9a
+Field_VerificationMethodOAuth=\u5916\u90e8 OAuth \u9274\u5b9a
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=\u7b54\u6848
 Long_Title_ActivateUser=\u6fc0\u6d3b\u9884\u914d\u7f6e\u5e10\u6237\uff0c\u521b\u5efa\u65b0\u53e3\u4ee4\u3002
 Long_Title_Admin=\u7ba1\u7406\u529f\u80fd

+ 12 - 1
src/main/resources/password/pwm/i18n/Display_zh_TW.properties

@@ -1,3 +1,4 @@
+#Tue, 20 Jun 2017 12:52:39 -0400
 #
 # Password Management Servlets (PWM)
 # http://www.pwm-project.org
@@ -55,6 +56,7 @@ Button_Show=\u986f\u793a
 Button_Show_Responses=\u986f\u793a\u56de\u61c9
 Button_Skip=\u8df3\u904e
 Button_SMS=\u7c21\u8a0a
+Button_TokenResend=\u91cd\u65b0\u50b3\u9001\u4ee3\u78bc
 Button_Unlock=\u89e3\u9664\u9396\u5b9a
 Button_UnlockPassword=\u89e3\u9664\u9396\u5b9a\u5bc6\u78bc
 Button_Update=\u66f4\u65b0
@@ -165,6 +167,7 @@ Display_SetupOtp_iPhone_Steps=<b>\u5728\u60a8\u7684 iPhone \u4e0a\uff0c\u9ede\u9
 Display_SetupOtp_Other_Title=\u5176\u4ed6
 Display_SetupOtp_Other_Steps=<b>\u5c0b\u627e\u76f8\u5bb9\u7684\u96d9\u56e0\u7d20\u61c9\u7528\u7a0b\u5f0f\u3002</b><ul><li>\u5617\u8a66\u5728\u88dd\u7f6e\u7684\u61c9\u7528\u7a0b\u5f0f\u5546\u5e97\u4e2d\u641c\u5c0b <b>Google Authenticator</b>\u3002<br/>\u8a31\u591a\u88dd\u7f6e\u90fd\u5177\u6709\u76f8\u5bb9\u7684\u61c9\u7528\u7a0b\u5f0f\u3002</li><li>\u5617\u8a66\u5c0b\u627e\u652f\u63f4\u300c<b>TOTP \u5b89\u5168\u6027\u8a18\u865f</b>\u300d\u6216\u300cRFC6238\u300d\u7684\u61c9\u7528\u7a0b\u5f0f</li><li>\u4e0b\u8f09\u4e26\u5b89\u88dd\u61c9\u7528\u7a0b\u5f0f\u3002</li></ul><b>\u7136\u5f8c\u958b\u555f\u4e26\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f\u3002</b><ol><li>\u6839\u64da\u61c9\u7528\u7a0b\u5f0f\u6307\u793a\uff0c\u5728\u4e0b\u65b9\u8f38\u5165\u8cc7\u6599\u6216\u6383\u63cf\u4ee3\u78bc\u3002</li><li>\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f\u5f8c\uff0c\u8acb\u6309\u4e00\u4e0b\u300c\u7e7c\u7e8c\u300d\u6309\u9215\u3002</li></ol>
 Display_TokenDestination=\u8a18\u865f\u76ee\u7684\u5730
+Display_TokenResend=\u60a8\u7684\u5b89\u5168\u4ee3\u78bc\u5c07\u7acb\u523b\u62b5\u9054\u3002\u82e5\u60a8\u5df2\u7b49\u5019\u591a\u6642\u4e14\u5c1a\u672a\u6536\u5230\u4ee3\u78bc\uff0c\u8acb\u6309\u4e00\u4e0b\u91cd\u65b0\u50b3\u9001\u4ee3\u78bc\u6309\u9215\u4ee5\u63a5\u6536\u65b0\u7684\u4ee3\u78bc\u3002
 Display_UsernameHeader=@User\:ID@
 Display_UsernameFooter=@User\:ID@
 Display_WarnExistingOtpSecretTime=\u60a8\u5df2\u65bc <span class\="timestamp">%1%</span> \u8a3b\u518a\u88dd\u7f6e\u3002\u60a8\u53ef\u4ee5\u5728\u4e0b\u65b9\u8f38\u5165\u7522\u751f\u7684\u4ee3\u78bc\uff0c\u4ee5\u6e2c\u8a66\u76ee\u524d\u7684\u88dd\u7f6e\u3002\u5982\u679c\u60a8\u7e7c\u7e8c\u9032\u884c\uff0c\u5247\u53ef\u4ee5\u91cd\u65b0\u8a2d\u5b9a\u76ee\u524d\u7684\u88dd\u7f6e\u3002
@@ -238,7 +241,15 @@ Field_VerificationMethodChallengeResponses=\u79d8\u5bc6\u554f\u984c\u548c\u7b54\
 Field_VerificationMethodAttributes=\u500b\u4eba\u8cc7\u6599
 Field_VerificationMethodRemoteResponses=\u5916\u90e8\u56de\u61c9
 Field_VerificationMethodNAAF=\u9032\u968e\u9a57\u8b49
-Field_VerificationMethodOAuth=\u5916\u90e8\u9a57\u8b49
+Field_VerificationMethodOAuth=\u5916\u90e8 OAuth \u9a57\u8b49
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=\u7b54\u6848
 Long_Title_ActivateUser=\u555f\u52d5\u9810\u5148\u8a2d\u5b9a\u7684\u5e33\u6236\u4e26\u5efa\u7acb\u65b0\u5bc6\u78bc\u3002
 Long_Title_Admin=\u7ba1\u7406\u529f\u80fd

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

@@ -158,6 +158,7 @@ Error_StartupError=Hi ha hagut un error mentre s'iniciava l'aplicaci\u00f3. Comp
 Error_EnvironmentError=Un error amb l'entorn de l'aplicaci\u00f3 ha fet que l'aplicaci\u00f3 no s'hagi iniciat.
 Error_ApplicationNotRunning=Aquesta funcionalitat no estar\u00e0 disponible fins que la configuraci\u00f3 de l'aplicaci\u00f3 s'hagi restringit.
 Error_EmailSendFailure=S'ha produ\u00eft un error en enviar l'element de correu electr\u00f2nic %1%. Error: %2%
+Error_PasswordOnlyBad=La contrasenya \u00e9s incorrecta; torneu-ho a provar.
 
 Error_RemoteErrorValue=Error remot: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Der opstod en fejl under opstart af programmet. Se oplysninge
 Error_EnvironmentError=En fejl med programmilj\u00f8et forhindrer programmet i at starte.
 Error_ApplicationNotRunning=Denne funktionalitet er ikke tilg\u00e6ngelig, f\u00f8r programkonfigurationen er begr\u00e6nset.
 Error_EmailSendFailure=Fejl under afsendelse af e-mailelementet %1%, fejl: %2%
+Error_PasswordOnlyBad=Adgangskoden er forkert. Pr\u00f8v igen.
 
 Error_RemoteErrorValue=Fjernfejl: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Fehler beim Starten der Anwendung. Weitere Informationen find
 Error_EnvironmentError=Ein Fehler der Anwendungsumgebung hat das Starten der Anwendung verhindert.
 Error_ApplicationNotRunning=Diese Funktion ist erst verf\u00fcgbar, wenn die Anwendungskonfiguration eingeschr\u00e4nkt ist.
 Error_EmailSendFailure=Fehler beim Senden des Email-Elements %1%, Fehler: %2%
+Error_PasswordOnlyBad=Falsches Passwort. Versuchen Sie es erneut.
 
 Error_RemoteErrorValue=Remotefehler: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Error al iniciar la aplicaci\u00f3n. Consulte los archivos de
 Error_EnvironmentError=Un error en el entorno de la aplicaci\u00f3n ha impedido que la aplicaci\u00f3n se inicie.
 Error_ApplicationNotRunning=Esta funcionalidad no estar\u00e1 disponible hasta que se limite la configuraci\u00f3n de la aplicaci\u00f3n.
 Error_EmailSendFailure=Error al enviar el elemento de correo electr\u00f3nico %1%, error: %2%
+Error_PasswordOnlyBad=Contrase\u00f1a incorrecta. Int\u00e9ntelo de nuevo.
 
 Error_RemoteErrorValue=Error remoto: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Une erreur s'est produite au d\u00e9marrage de l'application.
 Error_EnvironmentError=Une erreur li\u00e9e \u00e0 l'environnement de l'application a emp\u00each\u00e9 le d\u00e9marrage de cette derni\u00e8re.
 Error_ApplicationNotRunning=Cette fonctionnalit\u00e9 n'est pas disponible tant que la configuration de l'application est restreinte.
 Error_EmailSendFailure=Une erreur s'est produite lors de l'envoi du courrier \u00e9lectronique %1%. Erreur : %2%
+Error_PasswordOnlyBad=Le mot de passe est incorrect. Veuillez r\u00e9essayer.
 
 Error_RemoteErrorValue=Erreur distante : %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Errore nell'avvio dell'applicazione. Per informazioni, vedere
 Error_EnvironmentError=Errore nell'ambiente applicativo che ha impedito l'avvio dell'applicazione.
 Error_ApplicationNotRunning=La funzionalit\u00e0 non \u00e8 disponibile poich\u00e9 la configurazione dell'applicazione \u00e8 soggetta a restrizioni.
 Error_EmailSendFailure=Errore nell'invio dell'elemento e-mail %1%. Errore: %2%
+Error_PasswordOnlyBad=La password \u00e8 errata. Riprovare.
 
 Error_RemoteErrorValue=Errore remoto: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u958b\
 Error_EnvironmentError=\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u74b0\u5883\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u305f\u305f\u3081\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3067\u304d\u307e\u305b\u3093\u3002
 Error_ApplicationNotRunning=\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8a2d\u5b9a\u304c\u5236\u9650\u3055\u308c\u308b\u307e\u3067\u3001\u3053\u306e\u6a5f\u80fd\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002
 Error_EmailSendFailure=\u96fb\u5b50\u30e1\u30fc\u30eb\u9805\u76ee %1% \u306e\u9001\u4fe1\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30a8\u30e9\u30fc: %2%
+Error_PasswordOnlyBad=\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9593\u9055\u3063\u3066\u3044\u307e\u3059\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 
 Error_RemoteErrorValue=\u30ea\u30e2\u30fc\u30c8\u30a8\u30e9\u30fc: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Er is een fout opgetreden bij het starten van de toepassing.
 Error_EnvironmentError=Als gevolg van een fout met de toepassingsomgeving kan de toepassing niet worden gestart.
 Error_ApplicationNotRunning=Deze functionaliteit is pas beschikbaar als de toepassingsconfiguratie wordt beperkt.
 Error_EmailSendFailure=Fout bij verzenden van e-mailitem %1%, fout: %2%
+Error_PasswordOnlyBad=Het wachtwoord is onjuist. Probeer het opnieuw.
 
 Error_RemoteErrorValue=Externe fout: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Wyst\u0105pi\u0142 b\u0142\u0105d podczas uruchamiania aplika
 Error_EnvironmentError=B\u0142\u0105d \u015brodowiska aplikacji uniemo\u017cliwi\u0142 uruchomienie aplikacji.
 Error_ApplicationNotRunning=Ta funkcja jest niedost\u0119pna, dop\u00f3ki konfiguracja aplikacji jest ograniczona.
 Error_EmailSendFailure=B\u0142\u0105d podczas wysy\u0142ania elementu poczty e-mail %1%, b\u0142\u0105d: %2%
+Error_PasswordOnlyBad=Nieprawid\u0142owe has\u0142o. Spr\u00f3buj ponownie.
 
 Error_RemoteErrorValue=B\u0142\u0105d zdalny: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Erro ao iniciar o aplicativo. Consulte os arquivos de registr
 Error_EnvironmentError=O aplicativo n\u00e3o foi inicializado devido a um erro em seu ambiente.
 Error_ApplicationNotRunning=Essa funcionalidade n\u00e3o estar\u00e1 dispon\u00edvel enquanto a configura\u00e7\u00e3o de aplicativo estiver restrita.
 Error_EmailSendFailure=Erro ao enviar item de e-mail %1%, erro: %2%
+Error_PasswordOnlyBad=Senha incorreta, tente novamente.
 
 Error_RemoteErrorValue=Erro Remoto: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=\u041f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435
 Error_EnvironmentError=\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043e \u0438\u0437-\u0437\u0430 \u043e\u0448\u0438\u0431\u043a\u0438 \u0441\u043e \u0441\u0440\u0435\u0434\u043e\u0439 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.
 Error_ApplicationNotRunning=\u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u0430.
 Error_EmailSendFailure=\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b %1%, \u043e\u0448\u0438\u0431\u043a\u0430: %2%
+Error_PasswordOnlyBad=\u041f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.
 
 Error_RemoteErrorValue=\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=Ett fel intr\u00e4ffade n\u00e4r programmet startades. Mer in
 Error_EnvironmentError=Ett fel med programmilj\u00f6n hindrade programmet fr\u00e5n att starta.
 Error_ApplicationNotRunning=Denna funktion \u00e4r inte tillg\u00e4nglig f\u00f6rr\u00e4n programmets konfiguration begr\u00e4nsats.
 Error_EmailSendFailure=Fel n\u00e4r e-postobjekt skickades %1%, fel: %2%
+Error_PasswordOnlyBad=L\u00f6senordet \u00e4r felaktigt. F\u00f6rs\u00f6k igen.
 
 Error_RemoteErrorValue=Fj\u00e4rrfel: %1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=\u542f\u52a8\u8be5\u5e94\u7528\u7a0b\u5e8f\u65f6\u51fa\u9519\
 Error_EnvironmentError=\u5e94\u7528\u7a0b\u5e8f\u73af\u5883\u51fa\u9519\uff0c\u5bfc\u81f4\u8be5\u5e94\u7528\u7a0b\u5e8f\u65e0\u6cd5\u542f\u52a8\u3002
 Error_ApplicationNotRunning=\u5728\u5bf9\u5e94\u7528\u7a0b\u5e8f\u914d\u7f6e\u8fdb\u884c\u9650\u5236\u4e4b\u524d\uff0c\u6b64\u529f\u80fd\u4e0d\u53ef\u7528\u3002
 Error_EmailSendFailure=\u53d1\u9001\u7535\u5b50\u90ae\u4ef6\u9879 %1% \u65f6\u51fa\u9519\uff0c\u9519\u8bef\uff1a%2%
+Error_PasswordOnlyBad=\u53e3\u4ee4\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u8bd5\u3002
 
 Error_RemoteErrorValue=\u8fdc\u7a0b\u9519\u8bef\uff1a%1%
 

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

@@ -158,6 +158,7 @@ Error_StartupError=\u555f\u52d5\u61c9\u7528\u7a0b\u5f0f\u6642\u767c\u751f\u932f\
 Error_EnvironmentError=\u61c9\u7528\u7a0b\u5f0f\u74b0\u5883\u7684\u932f\u8aa4\u5df2\u9020\u6210\u61c9\u7528\u7a0b\u5f0f\u7121\u6cd5\u555f\u52d5\u3002
 Error_ApplicationNotRunning=\u8acb\u5148\u9650\u5236\u61c9\u7528\u7a0b\u5f0f\u7d44\u614b\uff0c\u624d\u80fd\u4f7f\u7528\u6b64\u529f\u80fd\u3002
 Error_EmailSendFailure=\u50b3\u9001\u96fb\u5b50\u90f5\u4ef6\u9805\u76ee\u6642\u767c\u751f\u932f\u8aa4 %1%\uff0c\u932f\u8aa4\uff1a%2%
+Error_PasswordOnlyBad=\u5bc6\u78bc\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002
 
 Error_RemoteErrorValue=\u9060\u7aef\u932f\u8aa4\uff1a%1%
 

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

@@ -158,6 +158,7 @@ Success_CreateGuest=El nou compte de convidat s'ha creat correctament. L'usuari
 Success_CreateUser=El nou compte d'usuari s'ha creat correctament.
 Success_NewUserForm=Tot est\u00e0 llest per crear el compte. Continu\u00ef quan estigui preparat.
 Success_PasswordChange=La contrasenya s'ha canviat correctament.
+Success_ChangedHelpdeskPassword=La contrasenya de l'usuari s'ha canviat correctament 
 Success_PasswordReset=La contrasenya de %1% s'ha definit correctament.
 Success_PasswordSend=La contrasenya nova s'ha enviat a %1%.  Tanqui la finestra i inici\u00ef la sessi\u00f3 amb la contrasenya nova.
 Success_ResponsesMeetRules=Les respostes compleixen els requisits. Cliqui Desa les respostes quan estigui preparat.
@@ -169,3 +170,4 @@ Success_UpdateProfile=La informaci\u00f3 d'usuari s'ha actualitzat correctament.
 Success_UpdateForm=Tot est\u00e0 llest per actualitzar el perfil. Continu\u00ef quan estigui preparat.
 Success_Action=L'acci\u00f3 %1% ha finalitzat correctament.
 Success_OtpSetup=La inscripci\u00f3 del dispositiu ha finalitzat correctament.
+Success_TokenResend=Us hem enviat un codi de seguretat nou.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=Den nye g\u00e6stekonto blev oprettet. G\u00e6stebrugeren mo
 Success_CreateUser=Din nye brugerkonto blev oprettet.
 Success_NewUserForm=Din konto er klar til at blive oprettet. Forts\u00e6t, n\u00e5r du er klar.
 Success_PasswordChange=Adgangskoden blev \u00e6ndret.
+Success_ChangedHelpdeskPassword=Adgangskoden blev \u00e6ndret for brugeren 
 Success_PasswordReset=Adgangskoden til %1% blev angivet.
 Success_PasswordSend=Din nye adgangskode blev sendt til %1%. Luk dette vindue, og log derefter p\u00e5 med din nye adgangskode.
 Success_ResponsesMeetRules=Dine svar overholder kravene. Klik p\u00e5 Gem svar, n\u00e5r du er klar.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Dine brugeroplysninger blev opdateret.
 Success_UpdateForm=Din profil er klar til at blive opdateret. Forts\u00e6t, n\u00e5r du er klar.
 Success_Action=Handlingen %1% blev gennemf\u00f8rt.
 Success_OtpSetup=Tilmeldingen af enheden blev gennemf\u00f8rt.
+Success_TokenResend=Der er sendt en ny sikkerhedskode til dig.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=Das neue Gastkonto wurde erfolgreich erstellt. Wenn eine Ema
 Success_CreateUser=Das neue Benutzerkonto wurde erfolgreich erstellt.
 Success_NewUserForm=Das Konto ist bereit zur Erstellung. Fahren Sie fort, wenn Sie bereit sind.
 Success_PasswordChange=Das Passwort wurde erfolgreich ge\u00e4ndert.
+Success_ChangedHelpdeskPassword=Passwort erfolgreich ge\u00e4ndert f\u00fcr Benutzer 
 Success_PasswordReset=Passwort f\u00fcr %1% wurde erfolgreich festgelegt.
 Success_PasswordSend=Das neue Passwort wurde an %1% gesendet. Schlie\u00dfen Sie dieses Fenster und melden Sie sich dann mit dem neuen Passwort an.
 Success_ResponsesMeetRules=Die Antworten erf\u00fcllen die Anforderungen. Klicken Sie auf 'Antworten speichern', wenn Sie bereit sind.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Die Benutzerinformationen wurden erfolgreich aktualisiert.
 Success_UpdateForm=Das Profil ist nun bereit zur Aktualisierung. Fahren Sie fort, wenn Sie bereit sind.
 Success_Action=Die Aktion %1% wurde erfolgreich abgeschlossen.
 Success_OtpSetup=Die Registrierung des Ger\u00e4ts wurde erfolgreich abgeschlossen.
+Success_TokenResend=Ihnen wurde ein neuer Sicherheitscode gesendet.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=La nueva cuenta de usuario guest se ha creado correctamente.
 Success_CreateUser=Su nueva cuenta de usuario se ha creado correctamente.
 Success_NewUserForm=Su cuenta est\u00e1 lista para ser creada. Contin\u00fae cuando est\u00e9 preparado.
 Success_PasswordChange=Se ha cambiado correctamente la contrase\u00f1a.
+Success_ChangedHelpdeskPassword=Se ha cambiado correctamente la contrase\u00f1a del usuario 
 Success_PasswordReset=La contrase\u00f1a para %1% se ha establecido correctamente.
 Success_PasswordSend=Se ha enviado su nueva contrase\u00f1a a %1%. Cierre esta ventana y luego entre utilizando la nueva contrase\u00f1a.
 Success_ResponsesMeetRules=Sus respuestas cumplen los requisitos. Haga clic en Guardar respuestas cuando est\u00e9 listo.
@@ -169,3 +170,4 @@ Success_UpdateProfile=La informaci\u00f3n del usuario se ha actualizado correcta
 Success_UpdateForm=Su perfil est\u00e1 listo para ser actualizado. Contin\u00fae cuando est\u00e9 preparado.
 Success_Action=La acci\u00f3n %1% se ha completado correctamente.
 Success_OtpSetup=Se ha realizado correctamente la inscripci\u00f3n de su dispositivo.
+Success_TokenResend=Le hemos enviado un c\u00f3digo de seguridad nuevo.

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

@@ -157,7 +157,8 @@ Success_ClearResponse=Vos questions et r\u00e9ponses secr\u00e8tes ont \u00e9t\u
 Success_CreateGuest=Le nouveau compte invit\u00e9 a \u00e9t\u00e9 cr\u00e9\u00e9. L'utilisateur invit\u00e9 va recevoir une notification si l'adresse \u00e9lectronique a \u00e9t\u00e9 entr\u00e9e. Le compte invit\u00e9 doit peut-\u00eatre encore \u00eatre activ\u00e9.
 Success_CreateUser=Votre nouveau compte utilisateur a \u00e9t\u00e9 cr\u00e9\u00e9.
 Success_NewUserForm=Votre compte est pr\u00eat \u00e0 \u00eatre cr\u00e9\u00e9. Continuez lorsque vous \u00eates pr\u00eat.
-Success_PasswordChange=Votre mot de passe a \u00e9t\u00e9 \u00e9dit\u00e9.
+Success_PasswordChange=Le mot de passe a \u00e9t\u00e9 modifi\u00e9.
+Success_ChangedHelpdeskPassword=Le mot de passe de l'utilisateur a \u00e9t\u00e9 modifi\u00e9 
 Success_PasswordReset=Le mot de passe pour %1% a \u00e9t\u00e9 d\u00e9fini.
 Success_PasswordSend=Votre nouveau mot de passe a \u00e9t\u00e9 envoy\u00e9 \u00e0 %1%. Fermez cette fen\u00eatre et connectez-vous \u00e0 l'aide de votre nouveau mot de passe.
 Success_ResponsesMeetRules=Vos r\u00e9ponses r\u00e9pondent aux exigences. Cliquez sur Enregistrer les r\u00e9ponses lorsque vous \u00eates pr\u00eat.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Vos informations utilisateur ont \u00e9t\u00e9 mises \u00e
 Success_UpdateForm=Votre profil est pr\u00eat \u00e0 \u00eatre mis \u00e0 jour. Continuez lorsque vous \u00eates pr\u00eat.
 Success_Action=L'op\u00e9ration %1% a \u00e9t\u00e9 effectu\u00e9e.
 Success_OtpSetup=L'inscription de votre p\u00e9riph\u00e9rique a \u00e9t\u00e9 effectu\u00e9e.
+Success_TokenResend=Nous venons de vous envoyer un nouveau code de s\u00e9curit\u00e9.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=Creazione del nuovo account guest eseguita. Se \u00e8 stato
 Success_CreateUser=Creazione del nuovo account utente eseguita.
 Success_NewUserForm=Account pronto per la creazione. Quando si \u00e8 pronti, continuare.
 Success_PasswordChange=Modifica della password eseguita.
+Success_ChangedHelpdeskPassword=Modifica della password utente eseguita 
 Success_PasswordReset=Impostazione della password per %1% eseguita.
 Success_PasswordSend=La nuova password \u00e8 stata inviata a %1%. Chiudere questa finestra ed eseguire il login utilizzando la nuova password.
 Success_ResponsesMeetRules=Le risposte soddisfano i requisiti. Quando si \u00e8 pronti, fare clic su Salva risposte.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Le informazioni dell'utente sono state aggiornate.
 Success_UpdateForm=Profilo pronto per l'aggiornamento. Quando si \u00e8 pronti, continuare.
 Success_Action=L'azione %1% \u00e8 stata eseguita.
 Success_OtpSetup=La registrazione del dispositivo \u00e8 stata eseguita.
+Success_TokenResend=Nuovo codice di sicurezza inviato.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=\u65b0\u3057\u3044\u30b2\u30b9\u30c8\u30a2\u30ab\u30a6\u30f3
 Success_CreateUser=\u65b0\u3057\u3044\u30e6\u30fc\u30b6\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u4f5c\u6210\u3055\u308c\u307e\u3057\u305f\u3002
 Success_NewUserForm=\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u4f5c\u6210\u6e96\u5099\u304c\u3067\u304d\u307e\u3057\u305f\u3002\u3044\u3064\u3067\u3082\u7d9a\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Success_PasswordChange=\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u5e38\u306b\u5909\u66f4\u3055\u308c\u307e\u3057\u305f\u3002
+Success_ChangedHelpdeskPassword=\u30e6\u30fc\u30b6\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u5e38\u306b\u5909\u66f4\u3055\u308c\u307e\u3057\u305f 
 Success_PasswordReset=%1% \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u5e38\u306b\u8a2d\u5b9a\u3055\u308c\u307e\u3057\u305f\u3002
 Success_PasswordSend=\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u304c %1% \u306b\u9001\u4fe1\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u3092\u9589\u3058\u3066\u304b\u3089\u3001\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Success_ResponsesMeetRules=\u56de\u7b54\u304c\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u3059\u3002\u3088\u308d\u3057\u3051\u308c\u3070\uff3b\u56de\u7b54\u306e\u4fdd\u5b58\uff3d\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002
@@ -169,3 +170,4 @@ Success_UpdateProfile=\u30e6\u30fc\u30b6\u60c5\u5831\u304c\u6b63\u5e38\u306b\u66
 Success_UpdateForm=\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u66f4\u65b0\u306e\u6e96\u5099\u304c\u3067\u304d\u307e\u3057\u305f\u3002\u3044\u3064\u3067\u3082\u7d9a\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Success_Action=\u30a2\u30af\u30b7\u30e7\u30f3 %1% \u304c\u6b63\u5e38\u306b\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002
 Success_OtpSetup=\u3054\u4f7f\u7528\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u304c\u6b63\u5e38\u306b\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002
+Success_TokenResend=\u65b0\u3057\u3044\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30b3\u30fc\u30c9\u304c\u9001\u4fe1\u3055\u308c\u307e\u3057\u305f\u3002

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

@@ -157,7 +157,8 @@ Success_ClearResponse=Uw geheime vragen en antwoorden zijn verwijderd.
 Success_CreateGuest=De nieuwe gastaccount is gemaakt. De gastgebruiker ontvangt een bericht als het e-mailadres is ingevoerd. Mogelijk moet de gastaccount nog worden geactiveerd.
 Success_CreateUser=Uw nieuwe gebruikersaccount is gemaakt.
 Success_NewUserForm=Uw account kan worden gemaakt. Ga verder als u gereed bent.
-Success_PasswordChange=Uw wachtwoord is gewijzigd.
+Success_PasswordChange=Het wachtwoord is gewijzigd.
+Success_ChangedHelpdeskPassword=Het wachtwoord voor de gebruiker is gewijzigd 
 Success_PasswordReset=Het wachtwoord voor %1% is ingesteld.
 Success_PasswordSend=Uw nieuwe wachtwoord is verzonden naar %1%. Sluit dit venster en gebruik het nieuwe wachtwoord om u opnieuw aan te melden.
 Success_ResponsesMeetRules=Uw antwoorden voldoen aan de vereisten. Klik op Antwoorden opslaan als u gereed bent.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Uw gebruikersgegevens zijn bijgewerkt.
 Success_UpdateForm=Uw profiel kan worden bijgewerkt. Ga verder als u gereed bent.
 Success_Action=De actie %1% is voltooid.
 Success_OtpSetup=Uw apparaatinschrijving is voltooid.
+Success_TokenResend=Er is een nieuwe beveiligingscode naar u verzonden.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=Nowe konto go\u015bcia zosta\u0142o pomy\u015blnie utworzone
 Success_CreateUser=Nowe konto u\u017cytkownika zosta\u0142o pomy\u015blnie utworzone.
 Success_NewUserForm=Konto jest gotowe do utworzenia. Mo\u017cesz kontynuowa\u0107.
 Success_PasswordChange=Has\u0142o zosta\u0142o pomy\u015blnie zmienione.
+Success_ChangedHelpdeskPassword=Has\u0142o zosta\u0142o pomy\u015blnie zmienione dla u\u017cytkownika 
 Success_PasswordReset=Has\u0142o dla u\u017cytkownika %1% zosta\u0142o pomy\u015blnie ustawione.
 Success_PasswordSend=Nowe has\u0142o zosta\u0142o wys\u0142ane na adres %1%. Zamknij to okno, a nast\u0119pnie zaloguj si\u0119 za pomoc\u0105 nowego has\u0142a.
 Success_ResponsesMeetRules=Te odpowiedzi spe\u0142niaj\u0105 wymagania. Mo\u017cesz klikn\u0105\u0107 przycisk Zapisz odpowiedzi.
@@ -169,3 +170,4 @@ Success_UpdateProfile=Informacje o u\u017cytkowniku zosta\u0142y pomy\u015blnie
 Success_UpdateForm=Profil jest gotowy do zaktualizowania. Mo\u017cesz kontynuowa\u0107.
 Success_Action=Dzia\u0142anie %1% zosta\u0142o pomy\u015blnie uko\u0144czone.
 Success_OtpSetup=Rejestracja urz\u0105dzenia zosta\u0142a pomy\u015blnie uko\u0144czona.
+Success_TokenResend=Wys\u0142ano Ci nowy kod zabezpiecze\u0144.

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

@@ -157,7 +157,8 @@ Success_ClearResponse=Suas perguntas e respostas secretas foram removidas com \u
 Success_CreateGuest=A nova conta do convidado foi criada com \u00eaxito. O usu\u00e1rio convidado receber\u00e1 uma notifica\u00e7\u00e3o se o endere\u00e7o de e-mail tiver sido informado. A conta do convidado pode ainda precisar ser ativada.
 Success_CreateUser=A nova conta do usu\u00e1rio foi criada com \u00eaxito.
 Success_NewUserForm=Sua conta est\u00e1 pronta para ser criada. Continue quando estiver pronto.
-Success_PasswordChange=Sua senha foi mudada com \u00eaxito.
+Success_PasswordChange=A senha foi mudada com \u00eaxito.
+Success_ChangedHelpdeskPassword=A senha do usu\u00e1rio foi mudada com \u00eaxito 
 Success_PasswordReset=A senha para %1% foi definida com \u00eaxito.
 Success_PasswordSend=Sua nova senha foi enviada para %1%. Feche esta janela e efetue login usando a nova senha.
 Success_ResponsesMeetRules=Suas respostas cumprem os requisitos. Clique em Gravar Respostas quando estiver pronto.
@@ -169,3 +170,4 @@ Success_UpdateProfile=As informa\u00e7\u00f5es do usu\u00e1rio foram atualizadas
 Success_UpdateForm=O seu perfil est\u00e1 pronto para ser atualizado. Continue quando estiver pronto.
 Success_Action=A a\u00e7\u00e3o %1% foi conclu\u00edda com \u00eaxito.
 Success_OtpSetup=A inscri\u00e7\u00e3o do dispositivo foi conclu\u00edda com \u00eaxito.
+Success_TokenResend=Um novo c\u00f3digo de seguran\u00e7a foi enviado.

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

@@ -158,6 +158,7 @@ Success_CreateGuest=\u041d\u043e\u0432\u0430\u044f \u0433\u043e\u0441\u0442\u043
 Success_CreateUser=\u041d\u043e\u0432\u0430\u044f \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0430.
 Success_NewUserForm=\u0412\u0441\u0435 \u0433\u043e\u0442\u043e\u0432\u043e, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435, \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b.
 Success_PasswordChange=\u041f\u0430\u0440\u043e\u043b\u044c \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d.
+Success_ChangedHelpdeskPassword=\u041f\u0430\u0440\u043e\u043b\u044c \u0431\u044b\u043b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f 
 Success_PasswordReset=\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f %1% \u0437\u0430\u0434\u0430\u043d \u0443\u0441\u043f\u0435\u0448\u043d\u043e.
 Success_PasswordSend=\u041d\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u043d\u0430 %1%. \u0417\u0430\u043a\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e \u043e\u043a\u043d\u043e \u0438 \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0441 \u043d\u043e\u0432\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.
 Success_ResponsesMeetRules=\u041e\u0442\u0432\u0435\u0442\u044b \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u044b", \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b.
@@ -169,3 +170,4 @@ Success_UpdateProfile=\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u04
 Success_UpdateForm=\u0412\u0441\u0435 \u0433\u043e\u0442\u043e\u0432\u043e, \u0447\u0442\u043e\u0431\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0444\u0438\u043b\u044c. \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435, \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b.
 Success_Action=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 %1% \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e.
 Success_OtpSetup=\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430.
+Success_TokenResend=\u041d\u043e\u0432\u044b\u0439 \u043a\u043e\u0434 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0432\u0430\u043c.

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