ソースを参照

Merge branch 'master' into xml-abstraction

jrivard@gmail.com 6 年 前
コミット
8fb6296ed9
47 ファイル変更391 行追加172 行削除
  1. 13 3
      docker/pom.xml
  2. 5 22
      pom.xml
  3. 1 1
      rest-test-service/pom.xml
  4. 22 2
      server/pom.xml
  5. 3 3
      server/src/main/java/password/pwm/PwmAboutProperty.java
  6. 2 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  7. 20 14
      server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java
  8. 9 7
      server/src/main/java/password/pwm/http/JspUtility.java
  9. 15 13
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  10. 11 11
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  11. 5 2
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java
  12. 35 3
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java
  13. 5 15
      server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java
  14. 2 2
      server/src/main/java/password/pwm/svc/node/NodeService.java
  15. 16 9
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java
  16. 5 3
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  17. 43 15
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyLdapStorageService.java
  18. 33 16
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  19. 11 4
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStorageService.java
  20. 1 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStoredJobState.java
  21. 2 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyUserStatus.java
  22. 16 6
      server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java
  23. 10 0
      server/src/main/java/password/pwm/util/i18n/LocaleHelper.java
  24. 5 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  25. 1 1
      server/src/main/resources/password/pwm/i18n/Display.properties
  26. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ca.properties
  27. 1 1
      server/src/main/resources/password/pwm/i18n/Error_da.properties
  28. 1 1
      server/src/main/resources/password/pwm/i18n/Error_de.properties
  29. 1 1
      server/src/main/resources/password/pwm/i18n/Error_en_CA.properties
  30. 1 1
      server/src/main/resources/password/pwm/i18n/Error_es.properties
  31. 1 1
      server/src/main/resources/password/pwm/i18n/Error_fr.properties
  32. 1 1
      server/src/main/resources/password/pwm/i18n/Error_fr_CA.properties
  33. 1 1
      server/src/main/resources/password/pwm/i18n/Error_it.properties
  34. 1 1
      server/src/main/resources/password/pwm/i18n/Error_iw.properties
  35. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ja.properties
  36. 1 1
      server/src/main/resources/password/pwm/i18n/Error_nl.properties
  37. 1 1
      server/src/main/resources/password/pwm/i18n/Error_pl.properties
  38. 1 1
      server/src/main/resources/password/pwm/i18n/Error_pt_BR.properties
  39. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ru.properties
  40. 1 1
      server/src/main/resources/password/pwm/i18n/Error_sv.properties
  41. 1 1
      server/src/main/resources/password/pwm/i18n/Error_zh_CN.properties
  42. 1 1
      server/src/main/resources/password/pwm/i18n/Error_zh_TW.properties
  43. 1 0
      server/src/main/resources/password/pwm/i18n/Message.properties
  44. 2 0
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  45. 42 0
      server/src/test/java/password/pwm/config/profile/PwmPasswordRuleTest.java
  46. 37 0
      webapp/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  47. 1 1
      webapp/src/main/webapp/public/health.jsp

+ 13 - 3
docker/pom.xml

@@ -20,6 +20,15 @@
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
     </properties>
 
+    <profiles>
+        <profile>
+            <id>skip-docker</id>
+            <properties>
+                <skipDocker>true</skipDocker>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
         <plugins>
             <plugin>
@@ -34,6 +43,7 @@
                             <goal>buildTar</goal>
                         </goals>
                         <configuration>
+                            <skip>${skipDocker}</skip>
                             <from>
                                 <image>adoptopenjdk/openjdk11</image>
                             </from>
@@ -51,15 +61,15 @@
                                 </volumes>
                             </container>
                             <extraDirectory>
-                                <path>${project.basedir}/src/main/image-files</path> <!-- Copies files from 'src/main/custom-extra-dir' -->
+                                <path>${project.basedir}/src/main/image-files</path>
                                 <permissions>
                                     <permission>
                                         <file>/app/startup.sh</file>
-                                        <mode>755</mode> <!-- Read/write for owner, read-only for group/other -->
+                                        <mode>755</mode>
                                     </permission>
                                     <permission>
                                         <file>/app/command.sh</file>
-                                        <mode>755</mode> <!-- Read/write for owner, read-only for group/other -->
+                                        <mode>755</mode>
                                     </permission>
                                 </permissions>
                             </extraDirectory>

+ 5 - 22
pom.xml

@@ -64,12 +64,6 @@
                 <skipSpotbugs>true</skipSpotbugs>
             </properties>
         </profile>
-        <profile>
-            <id>skip-tests</id>
-            <properties>
-                <skipTests>true</skipTests>
-            </properties>
-        </profile>
         <profile>
             <id>skip-javadoc</id>
             <properties>
@@ -80,17 +74,6 @@
 
     <build>
         <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.22.0</version>
-                <configuration>
-                    <skipTests>${skipTests}</skipTests>
-                    <excludes>
-                        <exclude>**/password.pwm.manual.*</exclude>
-                    </excludes>
-                </configuration>
-            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
@@ -188,7 +171,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.15</version>
+                        <version>8.16</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -235,12 +218,12 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.8</version>
+                <version>3.1.10</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.9</version>
+                        <version>3.1.10</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -266,7 +249,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>4.0.0</version>
+                <version>4.0.1</version>
                 <reportSets>
                     <reportSet>
                         <reports>
@@ -289,7 +272,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.9</version>
+            <version>3.1.10</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>

+ 1 - 1
rest-test-service/pom.xml

@@ -75,7 +75,7 @@
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
-            <version>2.19.0</version>
+            <version>2.20.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>

+ 22 - 2
server/pom.xml

@@ -19,8 +19,28 @@
         <skipTests>false</skipTests>
     </properties>
 
+    <profiles>
+        <profile>
+            <id>skip-tests</id>
+            <properties>
+                <skipTests>true</skipTests>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.0</version>
+                <configuration>
+                    <skipTests>${skipTests}</skipTests>
+                    <excludes>
+                        <exclude>**/password.pwm.manual.*</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>3.1.0</version>
@@ -119,7 +139,7 @@
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
-            <version>2.19.0</version>
+            <version>2.20.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -170,7 +190,7 @@
         <dependency>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
-            <version>0.7.3</version>
+            <version>0.7.4</version>
         </dependency>
         <dependency>
             <groupId>commons-net</groupId>

+ 3 - 3
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -89,9 +89,9 @@ public enum PwmAboutProperty
     java_vmLocation( "Java VM Location", pwmApplication -> System.getProperty( "java.home" ) ),
     java_vmVersion( "Java VM Version", pwmApplication -> System.getProperty( "java.vm.version" ) ),
     java_vmCommandLine( "Java VM Command Line", pwmApplication -> StringUtil.collectionToString( ManagementFactory.getRuntimeMXBean().getInputArguments() ) ),
-    java_osName( "Java OS Name", pwmApplication -> System.getProperty( "os.name" ) ),
-    java_osVersion( "Java OS Version", pwmApplication -> System.getProperty( "os.version" ) ),
-    java_osArch( "Java OS Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
+    java_osName( "Operating System Name", pwmApplication -> System.getProperty( "os.name" ) ),
+    java_osVersion( "Operating System Version", pwmApplication -> System.getProperty( "os.version" ) ),
+    java_osArch( "Operating System Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
     java_randomAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().pwmRandom().getAlgorithm() ),
     java_defaultCharset( null, pwmApplication -> Charset.defaultCharset().name() ),
     java_appServerInfo( "Java AppServer Info", pwmApplication -> pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() ),

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

@@ -491,6 +491,8 @@ public enum PwmSetting
             "password.policy.maximumAlpha", PwmSettingSyntax.NUMERIC, PwmSettingCategory.PASSWORD_POLICY ),
     PASSWORD_POLICY_MINIMUM_ALPHA(
             "password.policy.minimumAlpha", PwmSettingSyntax.NUMERIC, PwmSettingCategory.PASSWORD_POLICY ),
+    PASSWORD_POLICY_ALLOW_NON_ALPHA(
+            "password.policy.allowNonAlpha", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PASSWORD_POLICY ),
     PASSWORD_POLICY_MAXIMUM_NON_ALPHA(
             "password.policy.maximumNonAlpha", PwmSettingSyntax.NUMERIC, PwmSettingCategory.PASSWORD_POLICY ),
     PASSWORD_POLICY_MINIMUM_NON_ALPHA(

+ 20 - 14
server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java

@@ -280,6 +280,26 @@ public enum PwmPasswordRule
             ChaiPasswordRule.ADComplexityMaxViolation.getDefaultValue(),
             false ),
 
+    AllowNonAlpha(
+            ChaiPasswordRule.AllowNonAlpha,
+            PwmSetting.PASSWORD_POLICY_ALLOW_NON_ALPHA,
+            ChaiPasswordRule.AllowNonAlpha.getRuleType(),
+            ChaiPasswordRule.AllowNonAlpha.getDefaultValue(),
+            false ),
+
+    MinimumNonAlpha(
+            ChaiPasswordRule.MinimumNonAlpha,
+            PwmSetting.PASSWORD_POLICY_MINIMUM_NON_ALPHA,
+            ChaiPasswordRule.RuleType.MIN,
+            "0",
+            false ),
+
+    MaximumNonAlpha(
+            ChaiPasswordRule.MaximumNonAlpha,
+            PwmSetting.PASSWORD_POLICY_MAXIMUM_NON_ALPHA,
+            ChaiPasswordRule.RuleType.MAX,
+            "0",
+            false ),
 
     // pwm specific rules
     // value will be imported indirectly from chai rule
@@ -327,20 +347,6 @@ public enum PwmPasswordRule
             false
     ),
 
-    MinimumNonAlpha(
-            null,
-            PwmSetting.PASSWORD_POLICY_MINIMUM_NON_ALPHA,
-            ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
-
-    MaximumNonAlpha(
-            null,
-            PwmSetting.PASSWORD_POLICY_MAXIMUM_NON_ALPHA,
-            ChaiPasswordRule.RuleType.MAX,
-            "0",
-            false ),
-
     EnableWordlist(
             null,
             PwmSetting.PASSWORD_POLICY_ENABLE_WORDLIST,

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

@@ -26,7 +26,6 @@ import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.PwmSessionBean;
-import password.pwm.i18n.Display;
 import password.pwm.i18n.PwmDisplayBundle;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
@@ -171,9 +170,7 @@ public abstract class JspUtility
     public static String friendlyWrite( final PageContext pageContext, final boolean value )
     {
         final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
-        return value
-                ? LocaleHelper.getLocalizedMessage( Display.Value_True, pwmRequest )
-                : LocaleHelper.getLocalizedMessage( Display.Value_False, pwmRequest );
+        return LocaleHelper.valueBoolean( pwmRequest.getLocale(), value );
     }
 
     public static String friendlyWrite( final PageContext pageContext, final long value )
@@ -185,20 +182,25 @@ public abstract class JspUtility
 
     public static String friendlyWrite( final PageContext pageContext, final String input )
     {
-        final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
         if ( StringUtil.isEmpty( input ) )
         {
-            return LocaleHelper.getLocalizedMessage( Display.Value_NotApplicable, pwmRequest );
+            return friendlyWriteNotApplicable( pageContext );
         }
         return StringUtil.escapeHtml( input );
     }
 
+    public static String friendlyWriteNotApplicable( final PageContext pageContext )
+    {
+        final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
+        return LocaleHelper.valueNotApplicable( pwmRequest.getLocale() );
+    }
+
     public static String friendlyWrite( final PageContext pageContext, final Instant instant )
     {
         final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
         if ( instant == null )
         {
-            return LocaleHelper.getLocalizedMessage( Display.Value_NotApplicable, pwmRequest );
+            return LocaleHelper.valueNotApplicable( pwmRequest.getLocale() );
         }
         return "<span class=\"timestamp\">" + instant.toString() + "</span>";
     }

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

@@ -213,21 +213,23 @@ public class ClientApiServlet extends ControlledPwmServlet
     {
         if ( pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.RUNNING )
         {
-
-            if ( !pwmRequest.isAuthenticated() )
+            if ( !pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
             {
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_AUTHENTICATION_REQUIRED );
-                LOGGER.debug( pwmRequest, errorInformation );
-                pwmRequest.respondWithError( errorInformation );
-                return ProcessStatus.Halt;
-            }
+                if ( !pwmRequest.isAuthenticated() )
+                {
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_AUTHENTICATION_REQUIRED );
+                    LOGGER.debug( pwmRequest, errorInformation );
+                    pwmRequest.respondWithError( errorInformation );
+                    return ProcessStatus.Halt;
+                }
 
-            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
-            {
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, "admin privileges required" );
-                LOGGER.debug( pwmRequest, errorInformation );
-                pwmRequest.respondWithError( errorInformation );
-                return ProcessStatus.Halt;
+                if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
+                {
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, "admin privileges required" );
+                    LOGGER.debug( pwmRequest, errorInformation );
+                    pwmRequest.respondWithError( errorInformation );
+                    return ProcessStatus.Halt;
+                }
             }
         }
 

+ 11 - 11
server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java

@@ -56,7 +56,7 @@ import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.pwnotify.PwNotifyService;
-import password.pwm.svc.pwnotify.StoredJobState;
+import password.pwm.svc.pwnotify.PwNotifyStoredJobState;
 import password.pwm.svc.report.ReportCsvUtility;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.report.UserCacheRecord;
@@ -726,7 +726,7 @@ public class AdminServlet extends ControlledPwmServlet
         final Configuration config = pwmRequest.getConfig();
         final Locale locale = pwmRequest.getLocale();
         final PwNotifyService pwNotifyService = pwmRequest.getPwmApplication().getPwNotifyService();
-        final StoredJobState storedJobState = pwNotifyService.getJobState();
+        final PwNotifyStoredJobState pwNotifyStoredJobState = pwNotifyService.getJobState();
         final boolean canRunOnthisServer = pwNotifyService.canRunOnThisServer();
 
         statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.string,
@@ -741,30 +741,30 @@ public class AdminServlet extends ControlledPwmServlet
                     "Next Job Scheduled Time", LocaleHelper.instantString( pwNotifyService.getNextExecutionTime(), locale, config ) ) );
         }
 
-        if ( storedJobState != null )
+        if ( pwNotifyStoredJobState != null )
         {
             statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.timestamp,
-                    "Last Job Start Time", LocaleHelper.instantString( storedJobState.getLastStart(), locale, config ) ) );
+                    "Last Job Start Time", LocaleHelper.instantString( pwNotifyStoredJobState.getLastStart(), locale, config ) ) );
 
             statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.timestamp,
-                    "Last Job Completion Time", LocaleHelper.instantString( storedJobState.getLastCompletion(), locale, config ) ) );
+                    "Last Job Completion Time", LocaleHelper.instantString( pwNotifyStoredJobState.getLastCompletion(), locale, config ) ) );
 
-            if ( storedJobState.getLastStart() != null && storedJobState.getLastCompletion() != null )
+            if ( pwNotifyStoredJobState.getLastStart() != null && pwNotifyStoredJobState.getLastCompletion() != null )
             {
                 statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.timestamp,
-                        "Last Job Duration", TimeDuration.between( storedJobState.getLastStart(), storedJobState.getLastCompletion() ).asLongString( locale ) ) );
+                        "Last Job Duration", TimeDuration.between( pwNotifyStoredJobState.getLastStart(), pwNotifyStoredJobState.getLastCompletion() ).asLongString( locale ) ) );
             }
 
-            if ( !StringUtil.isEmpty( storedJobState.getServerInstance() ) )
+            if ( !StringUtil.isEmpty( pwNotifyStoredJobState.getServerInstance() ) )
             {
                 statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.string,
-                        "Last Job Server Instance", storedJobState.getServerInstance() ) );
+                        "Last Job Server Instance", pwNotifyStoredJobState.getServerInstance() ) );
             }
 
-            if ( storedJobState.getLastError() != null )
+            if ( pwNotifyStoredJobState.getLastError() != null )
             {
                 statusData.add( new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.string,
-                        "Last Job Error",  storedJobState.getLastError().toDebugStr() ) );
+                        "Last Job Error",  pwNotifyStoredJobState.getLastError().toDebugStr() ) );
             }
         }
 

+ 5 - 2
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java

@@ -23,17 +23,18 @@
 package password.pwm.http.servlet.admin;
 
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.Permission;
 import password.pwm.bean.pub.PublicUserInfoBean;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.ldap.UserInfo;
+import password.pwm.svc.pwnotify.PwNotifyUserStatus;
 
 import java.io.Serializable;
 import java.util.Map;
 
-@Getter
+@Value
 @Builder
 public class UserDebugDataBean implements Serializable
 {
@@ -47,4 +48,6 @@ public class UserDebugDataBean implements Serializable
     private final PwmPasswordPolicy ldapPasswordPolicy;
     private final PwmPasswordPolicy configuredPasswordPolicy;
     private final Map<ProfileType, String> profiles;
+
+    private final PwNotifyUserStatus pwNotifyUserStatus;
 }

+ 35 - 3
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java

@@ -38,6 +38,9 @@ import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
+import password.pwm.svc.PwmService;
+import password.pwm.svc.pwnotify.PwNotifyUserStatus;
+import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
 
@@ -45,10 +48,13 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.TreeMap;
 
 public class UserDebugDataReader
 {
+    private static final PwmLogger LOGGER = PwmLogger.forClass( UserDebugDataReader.class );
+
     public static UserDebugDataBean readUserDebugData(
             final PwmApplication pwmApplication,
             final Locale locale,
@@ -86,7 +92,9 @@ public class UserDebugDataReader
 
         final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, locale, sessionLabel, userIdentity );
 
-        final UserDebugDataBean userDebugData = UserDebugDataBean.builder()
+        final PwNotifyUserStatus pwNotifyUserStatus = readPwNotifyUserStatus( pwmApplication, userIdentity, sessionLabel );
+
+        return UserDebugDataBean.builder()
                 .userInfo( userInfo )
                 .publicUserInfoBean( PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine ) )
                 .permissions( permissions )
@@ -95,9 +103,8 @@ public class UserDebugDataReader
                 .configuredPasswordPolicy( configPasswordPolicy )
                 .passwordReadable( readablePassword )
                 .passwordWithinMinimumLifetime( userInfo.isWithinPasswordMinimumLifetime() )
+                .pwNotifyUserStatus( pwNotifyUserStatus )
                 .build();
-
-        return userDebugData;
     }
 
 
@@ -148,4 +155,29 @@ public class UserDebugDataReader
         }
         return Collections.unmodifiableMap( results );
     }
+
+    private static PwNotifyUserStatus readPwNotifyUserStatus(
+            final PwmApplication pwmApplication,
+            final UserIdentity userIdentity,
+            final SessionLabel sessionLabel
+    )
+    {
+        if ( pwmApplication.getPwNotifyService().status() == PwmService.STATUS.OPEN )
+        {
+            try
+            {
+                final Optional<PwNotifyUserStatus> value = pwmApplication.getPwNotifyService().readUserNotificationState( userIdentity, sessionLabel );
+                if ( value.isPresent() )
+                {
+                    return value.get();
+                }
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.debug( () -> "error reading user pwNotify status: " + e.getMessage() );
+            }
+        }
+
+        return null;
+    }
 }

+ 5 - 15
server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java

@@ -33,6 +33,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -51,24 +52,13 @@ class LDAPNodeDataService implements NodeDataServiceProvider
     {
         this.pwmApplication = pwmApplication;
 
-        final UserIdentity testUser;
-        final String ldapProfileID;
-        try
-        {
-            final LdapProfile ldapProfile = pwmApplication.getConfig().getDefaultLdapProfile();
-            ldapProfileID = ldapProfile.getIdentifier();
-            testUser = ldapProfile.getTestUser( pwmApplication );
-        }
-        catch ( PwmUnrecoverableException e )
-        {
-            final String msg = "error checking ldap test user configuration for ldap node service: " + e.getMessage();
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
-        }
+        final LdapProfile ldapProfile = pwmApplication.getConfig().getDefaultLdapProfile();
+        final String testUser = ldapProfile.readSettingAsString( PwmSetting.LDAP_TEST_USER_DN );
 
-        if ( testUser == null )
+        if ( StringUtil.isEmpty( testUser ) )
         {
             final String msg = "ldap node service requires that setting "
-                    + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfileID, null )
+                    + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), null )
                     + " is configured";
             throw PwmUnrecoverableException.newException( PwmError.ERROR_NODE_SERVICE_ERROR, msg );
         }

+ 2 - 2
server/src/main/java/password/pwm/svc/node/NodeService.java

@@ -114,11 +114,11 @@ public class NodeService implements PwmService
         catch ( PwmUnrecoverableException e )
         {
             startupError = e.getErrorInformation();
-            LOGGER.error( "error starting up cluster service: " + e.getMessage() );
+            LOGGER.error( "error starting up node service: " + e.getMessage() );
         }
         catch ( Exception e )
         {
-            startupError = new ErrorInformation( PwmError.ERROR_NODE_SERVICE_ERROR, "error starting up cluster service: " + e.getMessage() );
+            startupError = new ErrorInformation( PwmError.ERROR_NODE_SERVICE_ERROR, "error starting up node service: " + e.getMessage() );
             LOGGER.error( startupError );
         }
 

+ 16 - 9
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java

@@ -35,6 +35,8 @@ import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 
+import java.util.Optional;
+
 class PwNotifyDbStorageService implements PwNotifyStorageService
 {
     private static final String DB_STATE_STRING = "PwNotifyJobState";
@@ -54,7 +56,7 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
     }
 
     @Override
-    public StoredNotificationState readStoredUserState(
+    public Optional<PwNotifyUserStatus> readStoredUserState(
             final UserIdentity userIdentity,
             final SessionLabel sessionLabel
     )
@@ -84,13 +86,18 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, e.getMessage() ) );
         }
 
-        return JsonUtil.deserialize( rawDbValue, StoredNotificationState.class );
+        if ( !StringUtil.isEmpty( rawDbValue ) )
+        {
+            return Optional.ofNullable( JsonUtil.deserialize( rawDbValue, PwNotifyUserStatus.class ) );
+        }
+
+        return Optional.empty();
     }
 
     public void writeStoredUserState(
             final UserIdentity userIdentity,
             final SessionLabel sessionLabel,
-            final StoredNotificationState storedNotificationState
+            final PwNotifyUserStatus pwNotifyUserStatus
     )
             throws PwmUnrecoverableException
     {
@@ -108,7 +115,7 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
             throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
         }
 
-        final String rawDbValue = JsonUtil.serialize( storedNotificationState );
+        final String rawDbValue = JsonUtil.serialize( pwNotifyUserStatus );
         try
         {
             pwmApplication.getDatabaseAccessor().put( TABLE, guid, rawDbValue );
@@ -120,7 +127,7 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
     }
 
     @Override
-    public StoredJobState readStoredJobState()
+    public PwNotifyStoredJobState readStoredJobState()
             throws PwmUnrecoverableException
     {
         try
@@ -128,9 +135,9 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
             final String strValue = pwmApplication.getDatabaseService().getAccessor().get( DatabaseTable.PW_NOTIFY, DB_STATE_STRING );
             if ( StringUtil.isEmpty( strValue ) )
             {
-                return new StoredJobState( null, null, null, null, false );
+                return new PwNotifyStoredJobState( null, null, null, null, false );
             }
-            return JsonUtil.deserialize( strValue, StoredJobState.class );
+            return JsonUtil.deserialize( strValue, PwNotifyStoredJobState.class );
         }
         catch ( DatabaseException e )
         {
@@ -139,12 +146,12 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
     }
 
     @Override
-    public void writeStoredJobState( final StoredJobState storedJobState )
+    public void writeStoredJobState( final PwNotifyStoredJobState pwNotifyStoredJobState )
             throws PwmUnrecoverableException
     {
         try
         {
-            final String strValue = JsonUtil.serialize( storedJobState );
+            final String strValue = JsonUtil.serialize( pwNotifyStoredJobState );
             pwmApplication.getDatabaseService().getAccessor().put( DatabaseTable.PW_NOTIFY, DB_STATE_STRING, strValue );
         }
         catch ( DatabaseException e )

+ 5 - 3
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -52,6 +52,7 @@ import java.time.temporal.ChronoUnit;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -261,7 +262,7 @@ public class PwNotifyEngine
         }
 
         log( "sending notice to " + userIdentity.toDisplayString() + " for interval " + nextDayInterval );
-        storageService.writeStoredUserState( userIdentity, SESSION_LABEL, new StoredNotificationState( passwordExpirationTime, Instant.now(), nextDayInterval ) );
+        storageService.writeStoredUserState( userIdentity, SESSION_LABEL, new PwNotifyUserStatus( passwordExpirationTime, Instant.now(), nextDayInterval ) );
         sendNoticeEmail( userIdentity );
     }
 
@@ -296,13 +297,14 @@ public class PwNotifyEngine
     )
             throws PwmUnrecoverableException
     {
-        final StoredNotificationState storedState = storageService.readStoredUserState( userIdentity, SESSION_LABEL );
+        final Optional<PwNotifyUserStatus> optionalStoredState = storageService.readStoredUserState( userIdentity, SESSION_LABEL );
 
-        if ( storedState == null )
+        if ( !optionalStoredState.isPresent() )
         {
             return false;
         }
 
+        final PwNotifyUserStatus storedState = optionalStoredState.get();
         if ( storedState.getExpireTime() == null || !storedState.getExpireTime().equals( passwordExpirationTime ) )
         {
             return false;

+ 43 - 15
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyLdapStorageService.java

@@ -38,8 +38,14 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
 class PwNotifyLdapStorageService implements PwNotifyStorageService
 {
+    private static final String COR_GUID = ".";
+
     private final PwmApplication pwmApplication;
     private final PwNotifySettings settings;
 
@@ -67,11 +73,12 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
         this.pwmApplication = pwmApplication;
         this.settings = settings;
 
-        final UserIdentity userIdentity = pwmApplication.getConfig().getDefaultLdapProfile().getTestUser( pwmApplication );
-        if ( userIdentity == null )
+        final LdapProfile defaultLdapProfile = pwmApplication.getConfig().getDefaultLdapProfile();
+        final String testUserDN = defaultLdapProfile.readSettingAsString( PwmSetting.LDAP_TEST_USER_DN );
+        if ( StringUtil.isEmpty( testUserDN ) )
         {
             final String msg = "LDAP storage type selected, but LDAP test user ("
-                    + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( pwmApplication.getConfig().getDefaultLdapProfile().getIdentifier(), PwmConstants.DEFAULT_LOCALE )
+                    + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( defaultLdapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
                     + ") not defined.";
             throw new PwmUnrecoverableException( PwmError.ERROR_PWNOTIFY_SERVICE_ERROR, msg );
         }
@@ -90,7 +97,7 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
     }
 
     @Override
-    public StoredNotificationState readStoredUserState(
+    public Optional<PwNotifyUserStatus> readStoredUserState(
             final UserIdentity userIdentity,
             final SessionLabel sessionLabel
     )
@@ -98,22 +105,23 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
     {
         final ConfigObjectRecord configObjectRecord = getUserCOR( userIdentity, CoreType.User );
         final String payload = configObjectRecord.getPayload();
-        if ( StringUtil.isEmpty( payload ) )
+        if ( !StringUtil.isEmpty( payload ) )
         {
-            return JsonUtil.deserialize( payload, StoredNotificationState.class );
+            return Optional.ofNullable( JsonUtil.deserialize( payload, PwNotifyUserStatus.class ) );
         }
-        return null;
+
+        return Optional.empty();
     }
 
     public void writeStoredUserState(
             final UserIdentity userIdentity,
             final SessionLabel sessionLabel,
-            final StoredNotificationState storedNotificationState
+            final PwNotifyUserStatus pwNotifyUserStatus
     )
             throws PwmUnrecoverableException
     {
         final ConfigObjectRecord configObjectRecord = getUserCOR( userIdentity, CoreType.User );
-        final String payload = JsonUtil.serialize( storedNotificationState );
+        final String payload = JsonUtil.serialize( pwNotifyUserStatus );
         try
         {
             configObjectRecord.updatePayload( payload );
@@ -131,7 +139,7 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
     }
 
     @Override
-    public StoredJobState readStoredJobState()
+    public PwNotifyStoredJobState readStoredJobState()
             throws PwmUnrecoverableException
     {
         final UserIdentity proxyUser = pwmApplication.getConfig().getDefaultLdapProfile().getTestUser( pwmApplication );
@@ -140,18 +148,18 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
 
         if ( StringUtil.isEmpty( payload ) )
         {
-            return new StoredJobState( null, null, null, null, false );
+            return new PwNotifyStoredJobState( null, null, null, null, false );
         }
-        return JsonUtil.deserialize( payload, StoredJobState.class );
+        return JsonUtil.deserialize( payload, PwNotifyStoredJobState.class );
     }
 
     @Override
-    public void writeStoredJobState( final StoredJobState storedJobState )
+    public void writeStoredJobState( final PwNotifyStoredJobState pwNotifyStoredJobState )
             throws PwmUnrecoverableException
     {
         final UserIdentity proxyUser = pwmApplication.getConfig().getDefaultLdapProfile().getTestUser( pwmApplication );
         final ConfigObjectRecord configObjectRecord = getUserCOR( proxyUser, CoreType.ProxyUser );
-        final String payload = JsonUtil.serialize( storedJobState );
+        final String payload = JsonUtil.serialize( pwNotifyStoredJobState );
 
         try
         {
@@ -174,7 +182,27 @@ class PwNotifyLdapStorageService implements PwNotifyStorageService
     {
         final String userAttr = getLdapUserAttribute( userIdentity );
         final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser( userIdentity );
-        return ConfigObjectRecord.createNew( chaiUser, userAttr, coreType.getRecordID(), null, null );
+        try
+        {
+            final List<ConfigObjectRecord> list = ConfigObjectRecord.readRecordFromLDAP(
+                    chaiUser,
+                    userAttr,
+                    coreType.getRecordID(),
+                    Collections.singleton( COR_GUID ),
+                    Collections.singleton( COR_GUID ) );
+            if ( list.isEmpty() )
+            {
+                return ConfigObjectRecord.createNew( chaiUser, userAttr, coreType.getRecordID(), COR_GUID, COR_GUID );
+            }
+            else
+            {
+                return list.iterator().next();
+            }
+        }
+        catch ( ChaiUnavailableException | ChaiOperationException e )
+        {
+            throw PwmUnrecoverableException.fromChaiException( e );
+        }
     }
 
     private String getLdapUserAttribute( final UserIdentity userIdentity )

+ 33 - 16
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java

@@ -24,6 +24,7 @@ package password.pwm.svc.pwnotify;
 
 import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.error.ErrorInformation;
@@ -47,6 +48,7 @@ import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.ExecutorService;
 
 public class PwNotifyService extends AbstractPwmService implements PwmService
@@ -62,16 +64,16 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
 
     private DataStorageMethod storageMethod;
 
-    public StoredJobState getJobState() throws PwmUnrecoverableException
+    public PwNotifyStoredJobState getJobState() throws PwmUnrecoverableException
     {
         if ( status() != STATUS.OPEN )
         {
             if ( getStartupError() != null )
             {
-                return StoredJobState.builder().lastError( getStartupError() ).build();
+                return PwNotifyStoredJobState.builder().lastError( getStartupError() ).build();
             }
 
-            return StoredJobState.builder().build();
+            return PwNotifyStoredJobState.builder().build();
         }
 
         return storageService.readStoredJobState();
@@ -113,7 +115,7 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
         {
             if ( pwmApplication.getClusterService() == null || pwmApplication.getClusterService().status() != STATUS.OPEN )
             {
-                throw PwmUnrecoverableException.newException( PwmError.ERROR_PWNOTIFY_SERVICE_ERROR, "will remain closed, cluster service is not running" );
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_PWNOTIFY_SERVICE_ERROR, "will remain closed, node service is not running" );
             }
 
             settings = PwNotifySettings.fromConfiguration( pwmApplication.getConfig() );
@@ -174,17 +176,18 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
     private Instant figureNextJobExecutionTime()
             throws PwmUnrecoverableException
     {
-        final StoredJobState storedJobState = storageService.readStoredJobState();
-        if ( storedJobState != null )
+        final PwNotifyStoredJobState pwNotifyStoredJobState = storageService.readStoredJobState();
+        if ( pwNotifyStoredJobState != null )
         {
             // never run, or last job not successful.
-            if ( storedJobState.getLastCompletion() == null || storedJobState.getLastError() != null )
+            if ( pwNotifyStoredJobState.getLastCompletion() == null || pwNotifyStoredJobState.getLastError() != null )
             {
                 return Instant.now().plus( 1, ChronoUnit.MINUTES );
             }
 
             // more than 24hr ago.
-            if ( Duration.between( Instant.now(), storedJobState.getLastCompletion() ).abs().getSeconds() > settings.getMaximumSkipWindow().as( TimeDuration.Unit.SECONDS ) )
+            final long maxSeconds = settings.getMaximumSkipWindow().as( TimeDuration.Unit.SECONDS );
+            if ( Duration.between( Instant.now(), pwNotifyStoredJobState.getLastCompletion() ).abs().getSeconds() > maxSeconds )
             {
                 return Instant.now();
             }
@@ -220,10 +223,10 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
 
         try
         {
-            final StoredJobState storedJobState = storageService.readStoredJobState();
-            if ( storedJobState != null )
+            final PwNotifyStoredJobState pwNotifyStoredJobState = storageService.readStoredJobState();
+            if ( pwNotifyStoredJobState != null )
             {
-                final ErrorInformation errorInformation = storedJobState.getLastError();
+                final ErrorInformation errorInformation = pwNotifyStoredJobState.getLastError();
                 if ( errorInformation != null )
                 {
                     returnRecords.add( HealthRecord.forMessage( HealthMessage.PwNotify_Failure, errorInformation.toDebugStr() ) );
@@ -305,13 +308,13 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
             final Instant start = Instant.now();
             try
             {
-                storageService.writeStoredJobState( new StoredJobState( Instant.now(), null, pwmApplication.getInstanceID(), null, false ) );
+                storageService.writeStoredJobState( new PwNotifyStoredJobState( Instant.now(), null, pwmApplication.getInstanceID(), null, false ) );
                 StatisticsManager.incrementStat( pwmApplication, Statistic.PWNOTIFY_JOBS );
                 engine.executeJob();
 
                 final Instant finish = Instant.now();
-                final StoredJobState storedJobState = new StoredJobState( start, finish, pwmApplication.getInstanceID(), null, true );
-                storageService.writeStoredJobState( storedJobState );
+                final PwNotifyStoredJobState pwNotifyStoredJobState = new PwNotifyStoredJobState( start, finish, pwmApplication.getInstanceID(), null, true );
+                storageService.writeStoredJobState( pwNotifyStoredJobState );
             }
             catch ( Exception e )
             {
@@ -327,11 +330,11 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
 
                 final Instant finish = Instant.now();
                 final String instanceID = pwmApplication.getInstanceID();
-                final StoredJobState storedJobState = new StoredJobState( start, finish, instanceID, errorInformation, false );
+                final PwNotifyStoredJobState pwNotifyStoredJobState = new PwNotifyStoredJobState( start, finish, instanceID, errorInformation, false );
 
                 try
                 {
-                    storageService.writeStoredJobState( storedJobState );
+                    storageService.writeStoredJobState( pwNotifyStoredJobState );
                 }
                 catch ( Exception e2 )
                 {
@@ -343,4 +346,18 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
             }
         }
     }
+
+    public Optional<PwNotifyUserStatus> readUserNotificationState(
+            final UserIdentity userIdentity,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( status() == STATUS.OPEN )
+        {
+            return storageService.readStoredUserState( userIdentity, sessionLabel );
+        }
+
+        throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "pwnotify service is not open" );
+    }
 }

+ 11 - 4
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStorageService.java

@@ -26,20 +26,27 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.error.PwmUnrecoverableException;
 
+import java.util.Optional;
+
 interface PwNotifyStorageService
 {
 
-    StoredNotificationState readStoredUserState(
+    Optional<PwNotifyUserStatus> readStoredUserState(
             UserIdentity userIdentity,
             SessionLabel sessionLabel
     )
             throws PwmUnrecoverableException;
 
-    void writeStoredUserState( UserIdentity userIdentity, SessionLabel sessionLabel, StoredNotificationState storedNotificationState ) throws PwmUnrecoverableException;
+    void writeStoredUserState(
+            UserIdentity userIdentity,
+            SessionLabel sessionLabel,
+            PwNotifyUserStatus pwNotifyUserStatus
+    )
+            throws PwmUnrecoverableException;
 
-    StoredJobState readStoredJobState()
+    PwNotifyStoredJobState readStoredJobState()
             throws PwmUnrecoverableException;
 
-    void writeStoredJobState( StoredJobState storedJobState )
+    void writeStoredJobState( PwNotifyStoredJobState pwNotifyStoredJobState )
                     throws PwmUnrecoverableException;
 }

+ 1 - 1
server/src/main/java/password/pwm/svc/pwnotify/StoredJobState.java → server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStoredJobState.java

@@ -31,7 +31,7 @@ import java.time.Instant;
 
 @Value
 @Builder
-public class StoredJobState implements Serializable
+public class PwNotifyStoredJobState implements Serializable
 {
     private Instant lastStart;
     private Instant lastCompletion;

+ 2 - 1
server/src/main/java/password/pwm/svc/pwnotify/StoredNotificationState.java → server/src/main/java/password/pwm/svc/pwnotify/PwNotifyUserStatus.java

@@ -28,7 +28,8 @@ import java.io.Serializable;
 import java.time.Instant;
 
 @Value
-class StoredNotificationState implements Serializable
+public
+class PwNotifyUserStatus implements Serializable
 {
     private Instant expireTime;
     private Instant lastNotice;

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

@@ -885,15 +885,25 @@ public class PwmPasswordRuleValidator
         {
             final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
 
-            if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
+            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
-            }
+                if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
+                }
 
-            final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
-            if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
+                final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
+                if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                }
+            }
+            else
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                if ( numberOfNonAlphaChars > 0 )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                }
             }
         }
 

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

@@ -471,4 +471,14 @@ public class LocaleHelper
         return new ArrayList<>( returnMap.values() );
     }
 
+    public static String valueBoolean( final Locale locale, final boolean value )
+    {
+        final PwmDisplayBundle key = value ? Display.Value_True : Display.Value_False;
+        return getLocalizedMessage( locale, key, null );
+    }
+
+    public static String valueNotApplicable( final Locale locale )
+    {
+        return getLocalizedMessage( locale, Display.Value_NotApplicable, null );
+    }
 }

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

@@ -1193,6 +1193,11 @@
             <value>0</value>
         </default>
     </setting>
+    <setting hidden="false" key="password.policy.allowNonAlpha" level="1" required="true">
+        <default>
+            <value>true</value>
+        </default>
+    </setting>
     <setting hidden="false" key="password.policy.maximumNonAlpha" level="1" required="true">
         <default>
             <value>0</value>

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

@@ -373,7 +373,7 @@ Value_Default=Default
 Value_ProgressComplete=Complete
 Value_ProgressInProgress=In Progress
 Placeholder_Search=Search
-Instructions_ExportOrgChart1=Click the Export button to begin download of the organizational chart data. After download is complete, you can import the data into a program like Visio so it can be formatted into the desired output.
+Instructions_ExportOrgChart1=Click the Export button to begin download of the organizational chart data. After download is complete, you can use the data to import this org chart data into a program of your choice. The data is exported in CSV format.
 Instructions_ExportOrgChart2=Choose the export level depth, and press the Export button below.
 Instructions_EmailTeam1=The email list is generated based off of organizational data starting at this point. When you click the Send Email button, your default email program should automatically open, with the list of email addresses pre-filled.
 Instructions_EmailTeam2=Choose the team level depth, and press the Email button below.

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=La contrasenya \u00e9s incorrecta; torneu-ho a provar.
 Error_RecoverySequenceIncomplete=Hi ha hagut un problema durant la seq\u00fc\u00e8ncia per recuperar la contrasenya oblidada. Torni-ho a provar.
 Error_FileTypeIncorrect=El tipus de fitxer no \u00e9s correcte.
 Error_FileTooLarge=El fitxer \u00e9s massa gran.
-Error_ClusterServiceError=Hi ha hagut un error al servei del cl\u00faster: %1%.   Consulti els fitxers de registre per obtenir-ne m\u00e9s informaci\u00f3.
+Error_NodeServiceError=Hi ha hagut un error al servei del cl\u00faster: %1%.   Consulti els fitxers de registre per obtenir-ne m\u00e9s informaci\u00f3.
 Error_RemoteErrorValue=Error remot: %1%
 Error_WordlistImportError=Hi ha hagut un error en importar la llista de paraules: %1%
 Error_PwNotifyServiceError=Hi ha hagut un error en executar el servei de notificaci\u00f3 de contrasenyes: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Adgangskoden er forkert. Pr\u00f8v igen.
 Error_RecoverySequenceIncomplete=Der opstod et problem under sekvensen med den glemte adgangskode. Pr\u00f8v igen.
 Error_FileTypeIncorrect=Filtypen er ikke korrekt.
 Error_FileTooLarge=Filen er for stor.
-Error_ClusterServiceError=Der opstod en fejl med klyngetjenesten: %1%. Kontroll\u00e9r logfilerne for at f\u00e5 flere oplysninger.
+Error_NodeServiceError=Der opstod en fejl med klyngetjenesten: %1%. Kontroll\u00e9r logfilerne for at f\u00e5 flere oplysninger.
 Error_RemoteErrorValue=Fjernfejl: %1%
 Error_WordlistImportError=Der opstod en fejl under import af ordlisten: %1%
 Error_PwNotifyServiceError=Der opstod en fejl under k\u00f8rsel af tjenesten til notifikation om adgangskode: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Falsches Passwort. Versuchen Sie es erneut.
 Error_RecoverySequenceIncomplete=Fehler in der Sequenz f\u00fcr vergessene Passw\u00f6rter, versuchen Sie es erneut.
 Error_FileTypeIncorrect=Der Dateityp ist falsch.
 Error_FileTooLarge=Die Datei ist zu gro\u00df.
-Error_ClusterServiceError=Fehler beim Clusterservice: %1%. Weitere Informationen finden Sie in den Protokolldateien.
+Error_NodeServiceError=Fehler beim Clusterservice: %1%. Weitere Informationen finden Sie in den Protokolldateien.
 Error_RemoteErrorValue=Remotefehler: %1%
 Error_WordlistImportError=Fehler beim Importieren der Wortliste: %1%
 Error_PwNotifyServiceError=Fehler beim Ausf\u00fchren des Passwortbenachrichtigungsservice: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Password incorrect. Please try again.
 Error_RecoverySequenceIncomplete=A problem occurred during the forgotten password sequence. Please try again.
 Error_FileTypeIncorrect=The file type is not correct.
 Error_FileTooLarge=The file is too large.
-Error_ClusterServiceError=An error occurred with the cluster service: %1%.   Check the log files for more information.
+Error_NodeServiceError=An error occurred with the node service: %1%.   Check the log files for more information.
 Error_RemoteErrorValue=Remote Error: %1%
 Error_WordlistImportError=An error occurred while importing the wordlist: %1%
 Error_PwNotifyServiceError=An error occurred while running the password notify service: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Contrase\u00f1a incorrecta. Int\u00e9ntelo de nuevo.
 Error_RecoverySequenceIncomplete=Se ha producido un problema durante la secuencia de contrase\u00f1a olvidada; int\u00e9ntelo de nuevo.
 Error_FileTypeIncorrect=El tipo de archivo es incorrecto.
 Error_FileTooLarge=El archivo es demasiado grande.
-Error_ClusterServiceError=Se ha producido un error en el servicio de cl\u00faster: %1%. Consulte los archivos de registro para obtener m\u00e1s informaci\u00f3n.
+Error_NodeServiceError=Se ha producido un error en el servicio de cl\u00faster: %1%. Consulte los archivos de registro para obtener m\u00e1s informaci\u00f3n.
 Error_RemoteErrorValue=Error remoto: %1%
 Error_WordlistImportError=Se ha producido un error al importar la lista de palabras: %1%
 Error_PwNotifyServiceError=Se ha producido un error al ejecutar el servicio de notificaci\u00f3n de contrase\u00f1a: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Le mot de passe est incorrect. Veuillez r\u00e9essayer.
 Error_RecoverySequenceIncomplete=Un probl\u00e8me s'est produit lors de la s\u00e9quence de mot de passe oubli\u00e9, veuillez r\u00e9essayer.
 Error_FileTypeIncorrect=Le type de fichier n'est pas correct.
 Error_FileTooLarge=Le fichier est trop volumineux.
-Error_ClusterServiceError=Une erreur s'est produite avec le service de grappe : %1%. Consultez les fichiers journaux pour plus d'informations.
+Error_NodeServiceError=Une erreur s'est produite avec le service de grappe : %1%. Consultez les fichiers journaux pour plus d'informations.
 Error_RemoteErrorValue=Erreur distante : %1%
 Error_WordlistImportError=Une erreur s'est produite lors de l'importation de la liste de mots : %1%
 Error_PwNotifyServiceError=Une erreur s'est produite lors de l'ex\u00e9cution du service de notification de mot de passe : %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Le mot de passe est incorrect. Veuillez r\u00e9essayer.
 Error_RecoverySequenceIncomplete=Un probl\u00e8me s'est produit lors de l'oubli du mot de passe, veuillez r\u00e9essayer.
 Error_FileTypeIncorrect=Le type de fichier n'est pas correct.
 Error_FileTooLarge=Le fichier est trop volumineux.
-Error_ClusterServiceError=Une erreur s'est produite avec le service de grappe\u00a0: %1%.   Consultez les fichiers journaux pour plus d'informations.
+Error_NodeServiceError=Une erreur s'est produite avec le service de grappe\u00a0: %1%.   Consultez les fichiers journaux pour plus d'informations.
 Error_RemoteErrorValue=Erreur distante : %1%
 Error_WordlistImportError=Une erreur s'est produite lors de l'importation de la liste de mots\u00a0: %1%
 Error_PwNotifyServiceError=Une erreur s'est produite lors de l'ex\u00e9cution du service de notification de mot de passe\u00a0: %1%.

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=La password \u00e8 errata. Riprovare.
 Error_RecoverySequenceIncomplete=Si \u00e8 verificato un problema durante le sequenza della password dimenticata. Riprovare.
 Error_FileTypeIncorrect=Il tipo di file non \u00e8 corretto.
 Error_FileTooLarge=Il file \u00e8 troppo grande.
-Error_ClusterServiceError=Si \u00e8 verificato un errore con il servizio del cluster: %1%.   Verificare il file di log per ulteriori informazioni.
+Error_NodeServiceError=Si \u00e8 verificato un errore con il servizio del cluster: %1%.   Verificare il file di log per ulteriori informazioni.
 Error_RemoteErrorValue=Errore remoto: %1%
 Error_WordlistImportError=Si \u00e8 verificato un errore durante l'importazione dell'elenco di parole: %1%
 Error_PwNotifyServiceError=Si \u00e8 verificato un errore durante il servizio di notifica della password: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u0
 Error_RecoverySequenceIncomplete=\u05d0\u05d9\u05e8\u05e2\u05d4 \u05d1\u05e2\u05d9\u05d4 \u05d1\u05de\u05d4\u05dc\u05da \u05d4\u05e8\u05e6\u05e3 \u05e9\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05e9\u05db\u05d7\u05d4. \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.
 Error_FileTypeIncorrect=\u05e1\u05d5\u05d2 \u05d4\u05e7\u05d5\u05d1\u05e5 \u05e9\u05d2\u05d5\u05d9.
 Error_FileTooLarge=\u05d4\u05e7\u05d5\u05d1\u05e5 \u05d2\u05d3\u05d5\u05dc \u05de\u05d3\u05d9.
-Error_ClusterServiceError=\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e9\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05e9\u05db\u05d5\u05dc\u05d5\u05ea:  %1%.  \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d1\u05e6\u05d9 \u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.
+Error_NodeServiceError=\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e9\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05e9\u05db\u05d5\u05dc\u05d5\u05ea:  %1%.  \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d1\u05e6\u05d9 \u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.
 Error_RemoteErrorValue=\u05e9\u05d2\u05d9\u05d0\u05d4 \u05de\u05e8\u05d5\u05d7\u05e7\u05ea: %1%
 Error_WordlistImportError=\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05ea \u05d9\u05d9\u05d1\u05d5\u05d0 \u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05d9\u05dc\u05d9\u05dd: %1%
 Error_PwNotifyServiceError=\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05de\u05d4\u05dc\u05da \u05d4\u05e4\u05e2\u05dc\u05ea \u05e9\u05d9\u05e8\u05d5\u05ea \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea  \u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9593\u9055\u3063\u30
 Error_RecoverySequenceIncomplete=\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5fd8\u308c\u305f\u5834\u5408\u306e\u30b7\u30fc\u30b1\u30f3\u30b9\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Error_FileTypeIncorrect=\u30d5\u30a1\u30a4\u30eb\u306e\u7a2e\u985e\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002
 Error_FileTooLarge=\u30d5\u30a1\u30a4\u30eb\u304c\u5927\u304d\u3059\u304e\u307e\u3059\u3002
-Error_ClusterServiceError=\u30af\u30e9\u30b9\u30bf\u30b5\u30fc\u30d3\u30b9\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %1%\u3002\u8a73\u7d30\u306f\u3001\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+Error_NodeServiceError=\u30af\u30e9\u30b9\u30bf\u30b5\u30fc\u30d3\u30b9\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %1%\u3002\u8a73\u7d30\u306f\u3001\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Error_RemoteErrorValue=\u30ea\u30e2\u30fc\u30c8\u30a8\u30e9\u30fc: %1%
 Error_WordlistImportError=\u30ef\u30fc\u30c9\u30ea\u30b9\u30c8\u3092\u30a4\u30f3\u30dd\u30fc\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %1%
 Error_PwNotifyServiceError=\u30d1\u30b9\u30ef\u30fc\u30c9\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u3092\u5b9f\u884c\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Het wachtwoord is onjuist. Probeer het opnieuw.
 Error_RecoverySequenceIncomplete=Er is een fout opgetreden tijdens de vergeten wachtwoordreeks. Probeer het opnieuw.
 Error_FileTypeIncorrect=Het bestandstype is niet correct.
 Error_FileTooLarge=Het bestand is te groot.
-Error_ClusterServiceError=Er is een fout opgetreden voor de clusterservice: %1%. Raadpleeg de logbestanden voor meer informatie.
+Error_NodeServiceError=Er is een fout opgetreden voor de clusterservice: %1%. Raadpleeg de logbestanden voor meer informatie.
 Error_RemoteErrorValue=Externe fout: %1%
 Error_WordlistImportError=Er is een fout opgetreden bij het importeren van de woordenlijst: %1%
 Error_PwNotifyServiceError=Er is een fout opgetreden tijdens het uitvoeren van de wachtwoordwaarschuwingsservice: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Nieprawid\u0142owe has\u0142o. Spr\u00f3buj ponownie.
 Error_RecoverySequenceIncomplete=Wyst\u0105pi\u0142 problem podczas sekwencji zapomnianego has\u0142a. Spr\u00f3buj ponownie.
 Error_FileTypeIncorrect=Typ pliku nie jest poprawny.
 Error_FileTooLarge=Plik jest zbyt du\u017cy.
-Error_ClusterServiceError=Wyst\u0105pi\u0142 b\u0142\u0105d w us\u0142udze klastra: %1%. Sprawd\u017a pliki dziennika, aby uzyska\u0107 wi\u0119cej informacji.
+Error_NodeServiceError=Wyst\u0105pi\u0142 b\u0142\u0105d w us\u0142udze klastra: %1%. Sprawd\u017a pliki dziennika, aby uzyska\u0107 wi\u0119cej informacji.
 Error_RemoteErrorValue=B\u0142\u0105d zdalny: %1%
 Error_WordlistImportError=Wyst\u0105pi\u0142 b\u0142\u0105d podczas importowania listy s\u0142\u00f3w: %1%
 Error_PwNotifyServiceError=Wyst\u0105pi\u0142 b\u0142\u0105d podczas uruchamiania us\u0142ugi powiadamiania o ha\u015ble: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=Senha incorreta, tente novamente.
 Error_RecoverySequenceIncomplete=Houve um problema durante a sequ\u00eancia de senha esquecida, tente novamente.
 Error_FileTypeIncorrect=O tipo de arquivo n\u00e3o est\u00e1 correto.
 Error_FileTooLarge=O arquivo \u00e9 grande demais.
-Error_ClusterServiceError=Erro no servi\u00e7o de cluster: %1%. Verifique os arquivos de registro para obter mais informa\u00e7\u00f5es.
+Error_NodeServiceError=Erro no servi\u00e7o de cluster: %1%. Verifique os arquivos de registro para obter mais informa\u00e7\u00f5es.
 Error_RemoteErrorValue=Erro Remoto: %1%
 Error_WordlistImportError=Erro ao importar a lista de palavras: %1%
 Error_PwNotifyServiceError=Erro ao executar o servi\u00e7o de notifica\u00e7\u00e3o de senha: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=\u041f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043f\u0
 Error_RecoverySequenceIncomplete=\u041f\u0440\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043f\u043e \u043f\u043e\u0432\u043e\u0434\u0443 \u0437\u0430\u0431\u044b\u0442\u043e\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044f \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.
 Error_FileTypeIncorrect=\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f \u0444\u0430\u0439\u043b\u0430.
 Error_FileTooLarge=\u0424\u0430\u0439\u043b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0431\u043e\u043b\u044c\u0448\u043e\u0439.
-Error_ClusterServiceError=\u0412 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430: %1%. \u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0441\u043c. \u0432 \u0444\u0430\u0439\u043b\u0430\u0445 \u0436\u0443\u0440\u043d\u0430\u043b\u0430.
+Error_NodeServiceError=\u0412 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430: %1%. \u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0441\u043c. \u0432 \u0444\u0430\u0439\u043b\u0430\u0445 \u0436\u0443\u0440\u043d\u0430\u043b\u0430.
 Error_RemoteErrorValue=\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: %1%
 Error_WordlistImportError=\u041f\u0440\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0441\u043b\u043e\u0432 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430: %1%
 Error_PwNotifyServiceError=\u041f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u0430\u0440\u043e\u043b\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=L\u00f6senordet \u00e4r felaktigt. F\u00f6rs\u00f6k igen.
 Error_RecoverySequenceIncomplete=Ett fel intr\u00e4ffade under sekvensen f\u00f6r gl\u00f6mt l\u00f6senord. F\u00f6rs\u00f6k igen.
 Error_FileTypeIncorrect=Filtypen \u00e4r felaktig.
 Error_FileTooLarge=Filen \u00e4r f\u00f6r stor.
-Error_ClusterServiceError=Ett fel intr\u00e4ffade med klustertj\u00e4nsten: %1%. Se loggfilerna f\u00f6r mer information.
+Error_NodeServiceError=Ett fel intr\u00e4ffade med klustertj\u00e4nsten: %1%. Se loggfilerna f\u00f6r mer information.
 Error_RemoteErrorValue=Fj\u00e4rrfel: %1%
 Error_WordlistImportError=Ett fel intr\u00e4ffade n\u00e4r ordlistan skulle importeras: %1%
 Error_PwNotifyServiceError=Ett fel intr\u00e4ffade n\u00e4r aviseringstj\u00e4nsten f\u00f6r l\u00f6senord k\u00f6rdes: %1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=\u53e3\u4ee4\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u8bd5\u30
 Error_RecoverySequenceIncomplete=\u5fd8\u8bb0\u53e3\u4ee4\u64cd\u4f5c\u987a\u5e8f\u4e2d\u51fa\u9519\uff0c\u8bf7\u91cd\u8bd5\u3002
 Error_FileTypeIncorrect=\u6587\u4ef6\u7c7b\u578b\u9519\u8bef\u3002
 Error_FileTooLarge=\u6587\u4ef6\u8fc7\u5927\u3002
-Error_ClusterServiceError=\u7fa4\u96c6\u670d\u52a1\u51fa\u9519\uff1a%1%\u3002\u8bf7\u67e5\u770b\u65e5\u5fd7\u6587\u4ef6\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002
+Error_NodeServiceError=\u7fa4\u96c6\u670d\u52a1\u51fa\u9519\uff1a%1%\u3002\u8bf7\u67e5\u770b\u65e5\u5fd7\u6587\u4ef6\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002
 Error_RemoteErrorValue=\u8fdc\u7a0b\u9519\u8bef\uff1a%1%
 Error_WordlistImportError=\u5bfc\u5165\u5355\u8bcd\u8868\u65f6\u51fa\u9519\uff1a%1%
 Error_PwNotifyServiceError=\u8fd0\u884c\u53e3\u4ee4\u901a\u77e5\u670d\u52a1\u65f6\u51fa\u9519\uff1a%1%

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

@@ -162,7 +162,7 @@ Error_PasswordOnlyBad=\u5bc6\u78bc\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e
 Error_RecoverySequenceIncomplete=\u5fd8\u8a18\u5bc6\u78bc\u5e8f\u5217\u6642\u767c\u751f\u554f\u984c\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002
 Error_FileTypeIncorrect=\u6a94\u6848\u985e\u578b\u4e0d\u6b63\u78ba\u3002
 Error_FileTooLarge=\u6a94\u6848\u592a\u5927\u3002
-Error_ClusterServiceError=\u53e2\u96c6\u670d\u52d9\u767c\u751f\u932f\u8aa4\uff1a%1%\u3002\u8acb\u67e5\u770b\u8a18\u9304\u6a94\u4ee5\u53d6\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002
+Error_NodeServiceError=\u53e2\u96c6\u670d\u52d9\u767c\u751f\u932f\u8aa4\uff1a%1%\u3002\u8acb\u67e5\u770b\u8a18\u9304\u6a94\u4ee5\u53d6\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002
 Error_RemoteErrorValue=\u9060\u7aef\u932f\u8aa4\uff1a%1%
 Error_WordlistImportError=\u8f38\u5165\u55ae\u5b57\u6e05\u55ae\u6642\u767c\u751f\u932f\u8aa4\uff1a%1%
 Error_PwNotifyServiceError=\u57f7\u884c\u5bc6\u78bc\u901a\u77e5\u670d\u52d9\u767c\u751f\u932f\u8aa4\uff1a%1%

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

@@ -110,6 +110,7 @@ Rule_MinimumUpperCase=Minimum Upper Case
 Rule_MaximumUpperCase=Maximum Upper Case
 Rule_MinimumLowerCase=Minimum Lower Case
 Rule_MaximumLowerCase=Maximum Lower Case
+Rule_AllowNonAlpha=Allow Non-Alpha
 Rule_AllowNumeric=Allow Numeric
 Rule_MinimumNumeric=Minimum Numeric
 Rule_MaximumNumeric=Maximum Numeric

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

@@ -535,6 +535,7 @@ Setting_Description_password.policy.allowFirstCharNumeric=Enable this option to
 Setting_Description_password.policy.allowFirstCharSpecial=Enable this option to allow the first character of the password to be a special character.  Applies only if the password policy allows special characters.
 Setting_Description_password.policy.allowLastCharNumeric=Enable this option to allow the last character of the password to be numeric.  Applies only if the password policy allows numeric characters.
 Setting_Description_password.policy.allowLastCharSpecial=Enable this option to allow the last character of the password to be a special character.  Applies only if the password policy allows special characters.
+Setting_Description_password.policy.allowNonAlpha=Enable this option to allow non-alphabetic characters in the password.
 Setting_Description_password.policy.allowNumeric=Enable this option to allow numeric characters in the password.
 Setting_Description_password.policy.allowSpecial=Enable this option to allow special (non alpha-numeric) characters in the password.
 Setting_Description_password.policy.caseSensitivity=Enable this option to control if the password is case sensitive.  In most cases, @PwmAppName@ can read this from the directory, but in some cases, the system cannot correctly read this value, so you can override it here.
@@ -1054,6 +1055,7 @@ Setting_Label_password.policy.allowFirstCharNumeric=Allow First Character Numeri
 Setting_Label_password.policy.allowFirstCharSpecial=Allow First Character Special
 Setting_Label_password.policy.allowLastCharNumeric=Allow Last Character Numeric
 Setting_Label_password.policy.allowLastCharSpecial=Allow Last Character Special
+Setting_Label_password.policy.allowNonAlpha=Allow Non-Alphabetic Characters
 Setting_Label_password.policy.allowNumeric=Allow Numeric Characters
 Setting_Label_password.policy.allowSpecial=Allow Special Characters
 Setting_Label_password.policy.caseSensitivity=Password is Case Sensitive

+ 42 - 0
server/src/test/java/password/pwm/config/profile/PwmPasswordRuleTest.java

@@ -0,0 +1,42 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.profile;
+
+import org.junit.Test;
+import password.pwm.PwmConstants;
+
+public class PwmPasswordRuleTest
+{
+    @Test
+    public void testRuleLabels() throws Exception
+    {
+        for ( final PwmPasswordRule rule : PwmPasswordRule.values() )
+        {
+            final String value = rule.getLabel( PwmConstants.DEFAULT_LOCALE, null );
+            if ( value == null || value.contains( "MissingKey" ) )
+            {
+                throw new Exception(" missing label for PwmPasswordRule " + rule.name() );
+            }
+        }
+    }
+}

+ 37 - 0
webapp/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -33,6 +33,8 @@
 <%@ page import="java.util.Map" %>
 <%@ page import="password.pwm.util.java.TimeDuration" %>
 <%@ page import="password.pwm.util.i18n.LocaleHelper" %>
+<%@ page import="password.pwm.config.PwmSetting" %>
+<%@ page import="password.pwm.svc.PwmService" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -248,6 +250,41 @@
             </tr>
         </table>
         <br/>
+
+        <% if (JspUtility.getPwmRequest( pageContext ).getConfig().readSettingAsBoolean( PwmSetting.PW_EXPY_NOTIFY_ENABLE ) ) { %>
+        <table>
+            <tr>
+                <td colspan="10" class="title">Password Notification Status</td>
+            </tr>
+            <% if ( userDebugDataBean.getPwNotifyUserStatus() == null ) { %>
+            <tr>
+                <td class="key">Last Notification Sent</td>
+                <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
+            </tr>
+            <% } else { %>
+            <tr>
+                <td class="key">Last Notification Sent</td>
+                <td>
+                    <%=JspUtility.friendlyWrite(pageContext, userDebugDataBean.getPwNotifyUserStatus().getLastNotice())%>
+                </td>
+            </tr>
+            <tr>
+                <td class="key">Last Notification Password Expiration Time</td>
+                <td>
+                    <%=JspUtility.friendlyWrite(pageContext, userDebugDataBean.getPwNotifyUserStatus().getExpireTime())%>
+                </td>
+            </tr>
+            <tr>
+                <td class="key">Last Notification Interval</td>
+                <td>
+                    <%=userDebugDataBean.getPwNotifyUserStatus().getInterval()%>
+                </td>
+            </tr>
+            <% } %>
+        </table>
+        <br/>
+        <% } %>
+
         <table>
             <tr>
                 <td colspan="10" class="title">Applied Configuration</td>

+ 1 - 1
webapp/src/main/webapp/public/health.jsp

@@ -158,7 +158,7 @@
         }
 
         function handleWarnFlash() {
-            if (PWM_GLOBAL['pwm-health'] == "WARN") {
+            if (PWM_GLOBAL['pwm-health'] === "WARN") {
                 PWM_MAIN.flashDomElement(errorColor,'body',3000);
             }
         }