From c616f10a027e01f1669e2dbda59f5901f63a4cef Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Fri, 17 Jan 2025 13:05:40 +0100 Subject: [PATCH 1/6] Logout idle user automatically --- .../controller/SessionClientController.java | 20 ++++++ .../production/helper/ActivityMonitor.java | 49 ++++++++++++++ .../security/CustomLogoutSuccessHandler.java | 5 +- .../services/security/SessionService.java | 32 +-------- .../session/CustomHttpSessionListener.java | 65 +++++++++++++++++++ .../resources/messages/messages_de.properties | 2 + .../resources/messages/messages_en.properties | 2 + .../WEB-INF/resources/js/defaultScript.js | 18 +++++ .../main/webapp/WEB-INF/templates/base.xhtml | 26 ++++++++ 9 files changed, 185 insertions(+), 34 deletions(-) create mode 100644 Kitodo/src/main/java/org/kitodo/production/helper/ActivityMonitor.java create mode 100644 Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java diff --git a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java index 6d0fa97d36c..eafa8a43041 100644 --- a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java +++ b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java @@ -19,6 +19,7 @@ import javax.enterprise.context.RequestScoped; import javax.faces.context.FacesContext; import javax.inject.Named; +import javax.servlet.http.HttpSession; import org.kitodo.data.database.beans.Client; import org.kitodo.data.database.beans.Project; @@ -183,4 +184,23 @@ public List getAvailableClientsOfCurrentUserSortedByName() { return getAvailableClientsOfCurrentUser().stream().sorted(Comparator.comparing(Client::getName)) .collect(Collectors.toList()); } + + /** + * Get amount of time that warning message is displayed to inform user that he will be logged + * out of the system automatically due to inactivity. Value returned in seconds. + * If the session HTTP session timeout configured in the 'web.xml' file is 60 seconds or less, + * the message will be shown 30 seconds before logout. Otherwise, it will be shown 60 seconds in advance. + * @return number of seconds the warning message is displayed to the user before automatic logout + */ + public int getAutomaticLogoutWarningSeconds() { + FacesContext facesContext = FacesContext.getCurrentInstance(); + if (Objects.nonNull(facesContext)) { + HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false); + int maxInactiveInterval = session.getMaxInactiveInterval(); + if (maxInactiveInterval <= 60) { + return 30; + } + } + return 60; + } } diff --git a/Kitodo/src/main/java/org/kitodo/production/helper/ActivityMonitor.java b/Kitodo/src/main/java/org/kitodo/production/helper/ActivityMonitor.java new file mode 100644 index 00000000000..f9e2feb844e --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/helper/ActivityMonitor.java @@ -0,0 +1,49 @@ +/* + * (c) Kitodo. Key to digital objects e. V. + * + * This file is part of the Kitodo project. + * + * It is licensed under GNU General Public License version 3 or later. + * + * For the full copyright and license information, please read the + * GPL3-License.txt file that was distributed with this source code. + */ + +package org.kitodo.production.helper; + +import java.util.Iterator; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.inject.Named; + +import org.primefaces.PrimeFaces; + +@Named +@RequestScoped +public class ActivityMonitor { + + /** + * Event handler for 'idle' event. Triggered when user becomes idle and is about to be logged out automatically. + * Displays a warning message to inform the user he is about to get logged out soon. + */ + public void onIdle() { + String warningTitle = Helper.getTranslation("automaticLogoutWarningTitle"); + String warningDescription = Helper.getTranslation("automaticLogoutWarningDescription"); + PrimeFaces.current().executeScript("PF('sticky-notifications').renderMessage(" + + "{'summary':'" + warningTitle + "','detail':'" + warningDescription + "','severity':'error'});"); + } + + /** + * Event handler for 'active' event. Triggered when user becomes active again after being idle. + * Removes the warning message about pending automatic logout. + */ + public void onActive() { + Iterator messageIterator = FacesContext.getCurrentInstance().getMessages(); + while (messageIterator.hasNext()) { + messageIterator.next(); + messageIterator.remove(); + } + } +} diff --git a/Kitodo/src/main/java/org/kitodo/production/security/CustomLogoutSuccessHandler.java b/Kitodo/src/main/java/org/kitodo/production/security/CustomLogoutSuccessHandler.java index 3b6bb164144..4e5eda7fa65 100644 --- a/Kitodo/src/main/java/org/kitodo/production/security/CustomLogoutSuccessHandler.java +++ b/Kitodo/src/main/java/org/kitodo/production/security/CustomLogoutSuccessHandler.java @@ -12,7 +12,6 @@ package org.kitodo.production.security; import java.io.IOException; -import java.text.MessageFormat; import java.util.Objects; import javax.servlet.http.HttpServletRequest; @@ -53,8 +52,8 @@ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse resp UserDetails user = (UserDetails) principal; ServiceManager.getSessionService().expireSessionsOfUser(user); } else { - logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of UserDetails", - Helper.getObjectDescription(principal))); + logger.warn("Cannot expire session: {} is not an instance of UserDetails", + Helper.getObjectDescription(principal)); } } else { logger.warn("Cannot expire session: authentication.getDetails() is null"); diff --git a/Kitodo/src/main/java/org/kitodo/production/services/security/SessionService.java b/Kitodo/src/main/java/org/kitodo/production/services/security/SessionService.java index 34c3e0f44b9..4541629a378 100644 --- a/Kitodo/src/main/java/org/kitodo/production/services/security/SessionService.java +++ b/Kitodo/src/main/java/org/kitodo/production/services/security/SessionService.java @@ -11,30 +11,21 @@ package org.kitodo.production.services.security; -import java.text.MessageFormat; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.kitodo.production.helper.Helper; import org.kitodo.production.metadata.MetadataLock; import org.kitodo.production.security.SecurityConfig; import org.kitodo.production.security.SecuritySession; import org.kitodo.production.security.SecurityUserDetails; -import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.UserDetails; -public class SessionService implements HttpSessionListener { +public class SessionService { - private static final Logger logger = LogManager.getLogger(SessionService.class); private static volatile SessionService instance = null; private final SessionRegistry sessionRegistry; @@ -46,27 +37,6 @@ private SessionService() { this.sessionRegistry = securityConfig.getSessionRegistry(); } - /* - * This function is called when the session from the servlet container expires. - */ - @Override - public void sessionDestroyed(HttpSessionEvent se) { - Object securityContextObject = se.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); - if (securityContextObject instanceof SecurityContextImpl) { - SecurityContextImpl securityContext = (SecurityContextImpl) securityContextObject; - Object principal = securityContext.getAuthentication().getPrincipal(); - if (principal instanceof SecurityUserDetails) { - expireSessionsOfUser((SecurityUserDetails) principal); - } else { - logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of SecurityUserDetails", - Helper.getObjectDescription(principal))); - } - } else { - logger.warn(MessageFormat.format("Cannot expire session: {0} is not an instance of SecurityContextImpl", - Helper.getObjectDescription(securityContextObject))); - } - } - /** * Expires all active sessions of a spring security UserDetails object. * diff --git a/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java new file mode 100644 index 00000000000..2cba4d47d76 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java @@ -0,0 +1,65 @@ +/* + * (c) Kitodo. Key to digital objects e. V. + * + * This file is part of the Kitodo project. + * + * It is licensed under GNU General Public License version 3 or later. + * + * For the full copyright and license information, please read the + * GPL3-License.txt file that was distributed with this source code. + */ + + +package org.kitodo.production.session; + +import javax.servlet.annotation.WebListener; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.kitodo.production.helper.Helper; +import org.kitodo.production.security.SecurityUserDetails; +import org.kitodo.production.services.ServiceManager; +import org.springframework.security.core.context.SecurityContextImpl; + + +@WebListener +public class CustomHttpSessionListener implements HttpSessionListener { + + private static final Logger logger = LogManager.getLogger(CustomHttpSessionListener.class); + + /** + * Event handler that is triggere when an HTTP session is created. + * + * @param sessionEvent the notification event + */ + @Override + public void sessionCreated(HttpSessionEvent sessionEvent) { + logger.info("Session created: {}", sessionEvent.getSession().getId()); + } + + /** + * Event handler that is triggered when an HTTP session expires. + * + * @param sessionEvent the notification event + */ + @Override + public void sessionDestroyed(HttpSessionEvent sessionEvent) { + Object securityContextObject = sessionEvent.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); + if (securityContextObject instanceof SecurityContextImpl) { + SecurityContextImpl securityContext = (SecurityContextImpl) securityContextObject; + Object principal = securityContext.getAuthentication().getPrincipal(); + if (principal instanceof SecurityUserDetails) { + logger.info("Session expired: {}", sessionEvent.getSession().getId()); + ServiceManager.getSessionService().expireSessionsOfUser((SecurityUserDetails) principal); + } else { + logger.warn("Cannot expire session: {} is not an instance of SecurityUserDetails", + Helper.getObjectDescription(principal)); + } + } else { + logger.warn("Cannot expire session: {} is not an instance of SecurityContextImpl", + Helper.getObjectDescription(securityContextObject)); + } + } +} diff --git a/Kitodo/src/main/resources/messages/messages_de.properties b/Kitodo/src/main/resources/messages/messages_de.properties index c4113beb4ac..47c17ade5a3 100644 --- a/Kitodo/src/main/resources/messages/messages_de.properties +++ b/Kitodo/src/main/resources/messages/messages_de.properties @@ -62,6 +62,8 @@ actions=Aktionen audio=Audio automatic=automatisch automaticDmsImport=Automatischer DMS-Export +automaticLogoutWarningDescription=Aufgrund von Inaktivit\u00E4t werden Sie in K\u00FCrze automatisch ausgeloggt... +automaticLogoutWarningTitle=Keine Aktivit\u00E4t festgestellt automaticTask=Automatische Aufgabe automaticTasks=Automatische Aufgaben author=Autor diff --git a/Kitodo/src/main/resources/messages/messages_en.properties b/Kitodo/src/main/resources/messages/messages_en.properties index c74442431db..92674fedbdb 100644 --- a/Kitodo/src/main/resources/messages/messages_en.properties +++ b/Kitodo/src/main/resources/messages/messages_en.properties @@ -62,6 +62,8 @@ actions=Actions audio=Audio automatic=automatic automaticDmsImport=Automatic DMS export +automaticLogoutWarningDescription=Pending logout due to inactivity... +automaticLogoutWarningTitle=No activity registered automaticTask=Automatic task automaticTasks=Automatic tasks author=Author diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js index 42a5680572f..c7b64203ab2 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js @@ -12,3 +12,21 @@ $(document).ready(function() { $('#loadingScreen').hide(); }); + +window.updateLogoutCountdown = function(t) { + let growlMessage = $('#sticky-notifications_container div.ui-growl-message p') + let currentTime; + let minutes = Math.floor(t.current / 60); + let seconds = t.current % 60; + if (seconds < 10) { + currentTime = minutes + ":0" + seconds; + } else { + currentTime = minutes + ":" + seconds; + } + let currentMessage = growlMessage.text(); + if (currentMessage.match(/\d+:\d+/g)) { + growlMessage.text(currentMessage.replace(/\d+:\d+/g, currentTime)) + } else { + growlMessage.text(currentMessage + " " + currentTime); + } +} diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml index d7fc149ddb7..20ebc3c2d6c 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml @@ -17,6 +17,7 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:pe="http://primefaces.org/ui/extensions" xmlns:o="http://omnifaces.org/ui"> @@ -80,6 +81,31 @@ target="body" /> + + + + + + + + + From 25814f3eb4d69042540f5043060b0ecdfd94558a Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Fri, 24 Jan 2025 14:43:51 +0100 Subject: [PATCH 2/6] Cover case where HTTP session timeout is deactivated --- .../controller/SessionClientController.java | 8 +++++++- .../session/CustomHttpSessionListener.java | 12 +++++++----- Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java index eafa8a43041..65add8ab568 100644 --- a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java +++ b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java @@ -189,7 +189,9 @@ public List getAvailableClientsOfCurrentUserSortedByName() { * Get amount of time that warning message is displayed to inform user that he will be logged * out of the system automatically due to inactivity. Value returned in seconds. * If the session HTTP session timeout configured in the 'web.xml' file is 60 seconds or less, - * the message will be shown 30 seconds before logout. Otherwise, it will be shown 60 seconds in advance. + * the message will be shown 30 seconds before logout. If the timeout is between 1 and 5 minutes, + * the message will appear 60 seconds before logout. For any session timeout larger than 5 Minutes, + * it will be shown 300 seconds in advance. * @return number of seconds the warning message is displayed to the user before automatic logout */ public int getAutomaticLogoutWarningSeconds() { @@ -199,6 +201,10 @@ public int getAutomaticLogoutWarningSeconds() { int maxInactiveInterval = session.getMaxInactiveInterval(); if (maxInactiveInterval <= 60) { return 30; + } else if (maxInactiveInterval < 300) { + return 60; + } else { + return 300; } } return 60; diff --git a/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java index 2cba4d47d76..c1102278ae1 100644 --- a/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java +++ b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java @@ -23,6 +23,8 @@ import org.kitodo.production.services.ServiceManager; import org.springframework.security.core.context.SecurityContextImpl; +import java.util.Objects; + @WebListener public class CustomHttpSessionListener implements HttpSessionListener { @@ -36,7 +38,7 @@ public class CustomHttpSessionListener implements HttpSessionListener { */ @Override public void sessionCreated(HttpSessionEvent sessionEvent) { - logger.info("Session created: {}", sessionEvent.getSession().getId()); + logger.debug("Session created: {}", sessionEvent.getSession().getId()); } /** @@ -47,18 +49,18 @@ public void sessionCreated(HttpSessionEvent sessionEvent) { @Override public void sessionDestroyed(HttpSessionEvent sessionEvent) { Object securityContextObject = sessionEvent.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); - if (securityContextObject instanceof SecurityContextImpl) { + if (Objects.nonNull(securityContextObject) && securityContextObject instanceof SecurityContextImpl) { SecurityContextImpl securityContext = (SecurityContextImpl) securityContextObject; Object principal = securityContext.getAuthentication().getPrincipal(); if (principal instanceof SecurityUserDetails) { - logger.info("Session expired: {}", sessionEvent.getSession().getId()); + logger.debug("Session expired: {}", sessionEvent.getSession().getId()); ServiceManager.getSessionService().expireSessionsOfUser((SecurityUserDetails) principal); } else { - logger.warn("Cannot expire session: {} is not an instance of SecurityUserDetails", + logger.debug("Cannot expire session: {} is not an instance of SecurityUserDetails", Helper.getObjectDescription(principal)); } } else { - logger.warn("Cannot expire session: {} is not an instance of SecurityContextImpl", + logger.debug("Cannot expire session: {} is not an instance of SecurityContextImpl", Helper.getObjectDescription(securityContextObject)); } } diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml index 20ebc3c2d6c..aceb7fb1fab 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/base.xhtml @@ -82,7 +82,7 @@ + rendered="#{not empty SessionClientController.currentSessionClient and session.maxInactiveInterval gt 0}"> Date: Fri, 24 Jan 2025 16:38:10 +0100 Subject: [PATCH 3/6] Fix import order --- .../kitodo/production/session/CustomHttpSessionListener.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java index c1102278ae1..cb59b9ace88 100644 --- a/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java +++ b/Kitodo/src/main/java/org/kitodo/production/session/CustomHttpSessionListener.java @@ -12,6 +12,8 @@ package org.kitodo.production.session; +import java.util.Objects; + import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; @@ -23,8 +25,6 @@ import org.kitodo.production.services.ServiceManager; import org.springframework.security.core.context.SecurityContextImpl; -import java.util.Objects; - @WebListener public class CustomHttpSessionListener implements HttpSessionListener { From f0dcee871e0883f20a89415672268b21836d9e92 Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Fri, 24 Jan 2025 21:33:08 +0100 Subject: [PATCH 4/6] Load default script in metadata editor to show countdown before automatic logout --- Kitodo/src/main/webapp/pages/metadataEditor.xhtml | 1 + 1 file changed, 1 insertion(+) diff --git a/Kitodo/src/main/webapp/pages/metadataEditor.xhtml b/Kitodo/src/main/webapp/pages/metadataEditor.xhtml index 6e0ee6463d4..5e7919026c2 100644 --- a/Kitodo/src/main/webapp/pages/metadataEditor.xhtml +++ b/Kitodo/src/main/webapp/pages/metadataEditor.xhtml @@ -286,6 +286,7 @@ + From a2ddc4affc9c31649c7a01d8483aa05712e53f8f Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Mon, 27 Jan 2025 10:17:01 +0100 Subject: [PATCH 5/6] Add missing semicolon --- Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js index c7b64203ab2..2708b4951fb 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js @@ -14,7 +14,7 @@ $(document).ready(function() { }); window.updateLogoutCountdown = function(t) { - let growlMessage = $('#sticky-notifications_container div.ui-growl-message p') + let growlMessage = $('#sticky-notifications_container div.ui-growl-message p'); let currentTime; let minutes = Math.floor(t.current / 60); let seconds = t.current % 60; From 56e714585c37d67c3272f52e53f73ce5f8bac39b Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Mon, 27 Jan 2025 10:46:07 +0100 Subject: [PATCH 6/6] Add more missing semicolons --- Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js index 2708b4951fb..deb540679ab 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/defaultScript.js @@ -25,8 +25,8 @@ window.updateLogoutCountdown = function(t) { } let currentMessage = growlMessage.text(); if (currentMessage.match(/\d+:\d+/g)) { - growlMessage.text(currentMessage.replace(/\d+:\d+/g, currentTime)) + growlMessage.text(currentMessage.replace(/\d+:\d+/g, currentTime)); } else { growlMessage.text(currentMessage + " " + currentTime); } -} +};