فهرست منبع

pwnotify ldap updates and user debug output

jrivard@gmail.com 6 سال پیش
والد
کامیت
e6b86941d1
32فایلهای تغییر یافته به همراه241 افزوده شده و 106 حذف شده
  1. 9 7
      server/src/main/java/password/pwm/http/JspUtility.java
  2. 11 11
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  3. 5 2
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java
  4. 35 3
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java
  5. 5 15
      server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java
  6. 2 2
      server/src/main/java/password/pwm/svc/node/NodeService.java
  7. 16 9
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java
  8. 5 3
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  9. 43 15
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyLdapStorageService.java
  10. 32 16
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  11. 11 4
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStorageService.java
  12. 1 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStoredJobState.java
  13. 2 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyUserStatus.java
  14. 10 0
      server/src/main/java/password/pwm/util/i18n/LocaleHelper.java
  15. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ca.properties
  16. 1 1
      server/src/main/resources/password/pwm/i18n/Error_da.properties
  17. 1 1
      server/src/main/resources/password/pwm/i18n/Error_de.properties
  18. 1 1
      server/src/main/resources/password/pwm/i18n/Error_en_CA.properties
  19. 1 1
      server/src/main/resources/password/pwm/i18n/Error_es.properties
  20. 1 1
      server/src/main/resources/password/pwm/i18n/Error_fr.properties
  21. 1 1
      server/src/main/resources/password/pwm/i18n/Error_fr_CA.properties
  22. 1 1
      server/src/main/resources/password/pwm/i18n/Error_it.properties
  23. 1 1
      server/src/main/resources/password/pwm/i18n/Error_iw.properties
  24. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ja.properties
  25. 1 1
      server/src/main/resources/password/pwm/i18n/Error_nl.properties
  26. 1 1
      server/src/main/resources/password/pwm/i18n/Error_pl.properties
  27. 1 1
      server/src/main/resources/password/pwm/i18n/Error_pt_BR.properties
  28. 1 1
      server/src/main/resources/password/pwm/i18n/Error_ru.properties
  29. 1 1
      server/src/main/resources/password/pwm/i18n/Error_sv.properties
  30. 1 1
      server/src/main/resources/password/pwm/i18n/Error_zh_CN.properties
  31. 1 1
      server/src/main/resources/password/pwm/i18n/Error_zh_TW.properties
  32. 37 0
      webapp/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

+ 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>";
     }

+ 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 )

+ 32 - 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,17 @@ 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 ) )
+            if ( Duration.between( Instant.now(), pwNotifyStoredJobState.getLastCompletion() ).abs().getSeconds() > settings.getMaximumSkipWindow().as( TimeDuration.Unit.SECONDS ) )
             {
                 return Instant.now();
             }
@@ -220,10 +222,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 +307,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 +329,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 +345,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;

+ 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 );
+    }
 }

+ 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%

+ 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>