diff --git a/build.gradle b/build.gradle index f35fbacd..868ee476 100644 --- a/build.gradle +++ b/build.gradle @@ -536,6 +536,9 @@ dependencies { annotationProcessor "org.projectlombok:lombok:$lombokVersion" testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' + // https://mvnrepository.com/artifact/org.springframework/spring-webflux + implementation("org.springframework.boot:spring-boot-starter-webflux") + } tasks.withType(JavaCompile).configureEach { diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index da97136e..562f847d 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -39,14 +39,14 @@ public class EndpointInterceptor implements HandlerInterceptor { } String requestURI = request.getRequestURI(); - if ("GET".equalsIgnoreCase(request.getMethod())) { + boolean isApiRequest = requestURI.contains("/api/v1"); + + if ("GET".equalsIgnoreCase(request.getMethod()) && !isApiRequest) { Principal principal = request.getUserPrincipal(); - boolean isApiRequest = requestURI.contains("/api/v1"); - // allowlist for public or static routes - if (("/".equals(requestURI) + if ("/".equals(requestURI) || "/login".equals(requestURI) || "/home".equals(requestURI) || "/home-legacy".equals(requestURI) @@ -61,9 +61,9 @@ public class EndpointInterceptor implements HandlerInterceptor { || requestURI.endsWith(".js") || requestURI.endsWith(".png") || requestURI.endsWith(".webmanifest") - || requestURI.contains("/files/")) && !isApiRequest) { + || requestURI.contains("/files/")) { return true; - } else if (principal != null && !isApiRequest) { + } else if (principal != null) { if (session == null) { session = request.getSession(true); } @@ -71,6 +71,38 @@ public class EndpointInterceptor implements HandlerInterceptor { final HttpSession finalSession = session; String sessionId = finalSession.getId(); + boolean isExpiredByAdmin = + sessionsInterface.getAllSessions().stream() + .filter(s -> s.getSessionId().equals(finalSession.getId())) + .anyMatch(s -> s.isExpired()); + + if (isExpiredByAdmin) { + response.sendRedirect("/logout"); + log.info("Session expired. Logging out user {}", principal.getName()); + return false; + } + + int maxApplicationSessions = sessionsInterface.getMaxApplicationSessions(); + + Collection allSessions = sessionsInterface.getAllSessions(); + + long totalSessionsNonExpired = + allSessions.stream().filter(s -> !s.isExpired()).count(); + + List activeSessions = + allSessions.stream() + .filter(s -> !s.isExpired()) + .sorted( + (s1, s2) -> + Long.compare( + s2.getLastRequest().getTime(), + s1.getLastRequest().getTime())) + .limit(maxApplicationSessions) + .toList(); + + boolean hasUserActiveSession = + activeSessions.stream().anyMatch(s -> s.getSessionId().equals(sessionId)); + final String currentPrincipal = principal.getName(); long userSessions = @@ -82,29 +114,21 @@ public class EndpointInterceptor implements HandlerInterceptor { s.getPrincipalName())) .count(); - long totalSessions = - sessionsInterface.getAllSessions().stream() - .filter(s -> !s.isExpired()) - .count(); - int maxUserSessions = sessionsInterface.getMaxUserSessions(); log.info( - "Active sessions for {}: {} (max: {}) | Total: {} (max: {})", + "Active sessions for {}: {} (max: {}) | Total: {} (max: {}) | Active" + + " sessions: {}", currentPrincipal, userSessions, maxUserSessions, - totalSessions, - sessionsInterface.getMaxApplicationSessions()); - - boolean isCurrentSessionRegistered = - sessionsInterface.getAllSessions().stream() - .filter(s -> !s.isExpired()) - .anyMatch(s -> s.getSessionId().equals(sessionId)); + totalSessionsNonExpired, + maxApplicationSessions, + hasUserActiveSession); if ((userSessions >= maxUserSessions - || totalSessions >= sessionsInterface.getMaxApplicationSessions()) - && !isCurrentSessionRegistered) { + || totalSessionsNonExpired >= maxApplicationSessions) + && !hasUserActiveSession) { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Max sessions reached for this user. To continue on this device, please" @@ -114,15 +138,15 @@ public class EndpointInterceptor implements HandlerInterceptor { // If session is not registered yet, register it; otherwise, update the last request // timestamp. - if (!isCurrentSessionRegistered) { - log.debug("Register session: {}", sessionId); + if (!hasUserActiveSession) { + log.info("Register session: {}", sessionId); sessionsInterface.registerSession(finalSession); } else { - log.debug("Update session last request: {}", sessionId); + log.info("Update session last request: {}", sessionId); sessionsInterface.updateSessionLastRequest(sessionId); } return true; - } else if (principal == null && !isApiRequest) { + } else if (principal == null) { if (session == null) { session = request.getSession(true); } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java index 7e9fb1e3..a6b43cde 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java @@ -89,6 +89,10 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI sessionPersistentRegistry.expireSession(sessionId); } + public void expireSession(String sessionId, boolean expiredByAdmin) { + sessionPersistentRegistry.expireSession(sessionId, expiredByAdmin); + } + public int getMaxInactiveInterval() { return (int) defaultMaxInactiveInterval.getSeconds(); } @@ -139,7 +143,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI .toList() .size(); boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled; - log.debug( + log.info( "all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}", all, allNonExpiredSessions, @@ -147,7 +151,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI isAnonymousUserWithoutLogin); if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) { - log.debug("Session {} Expired=TRUE", session.getId()); + log.info("Session {} Expired=TRUE", session.getId()); sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); // if (allNonExpiredSessions > getMaxUserSessions()) { @@ -155,12 +159,12 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI // } } else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) { enforceMaxSessionsForPrincipal(principalName); - log.debug("Session {} Expired=TRUE", session.getId()); + log.info("Session {} Expired=TRUE", session.getId()); } else if (isAnonymousUserWithoutLogin) { sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); } else { - log.debug("Session created: {}", session.getId()); + log.info("Session created: {}", session.getId()); sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index 2edeaa0c..085d209f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -28,14 +28,18 @@ public class SessionPersistentRegistry implements SessionRegistry { private final SessionRepository sessionRepository; private final boolean runningEE; + private final boolean loginEnabled; @Value("${server.servlet.session.timeout:30m}") private Duration defaultMaxInactiveInterval; public SessionPersistentRegistry( - SessionRepository sessionRepository, @Qualifier("runningEE") boolean runningEE) { + SessionRepository sessionRepository, + @Qualifier("runningEE") boolean runningEE, + @Qualifier("loginEnabled") boolean loginEnabled) { this.runningEE = runningEE; this.sessionRepository = sessionRepository; + this.loginEnabled = loginEnabled; } @Override @@ -86,7 +90,7 @@ public class SessionPersistentRegistry implements SessionRegistry { if (sessionEntity == null) { sessionEntity = new SessionEntity(); sessionEntity.setSessionId(sessionId); - log.debug("Registering new session for principal: {}", principalName); + log.info("Registering new session for principal: {}", principalName); } sessionEntity.setPrincipalName(principalName); sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date @@ -188,6 +192,17 @@ public class SessionPersistentRegistry implements SessionRegistry { } } + public void expireSession(String sessionId, boolean expiredByAdmin) { + Optional sessionEntityOpt = sessionRepository.findById(sessionId); + if (sessionEntityOpt.isPresent()) { + SessionEntity sessionEntity = sessionEntityOpt.get(); + sessionEntity.setExpired(true); // Set expired to true + sessionEntity.setAdminExpired(expiredByAdmin); + sessionRepository.save(sessionEntity); + log.debug("Session expired: {}", sessionId); + } + } + // Mark all sessions as expired public void expireAllSessions() { List sessionEntities = sessionRepository.findAll(); @@ -283,9 +298,12 @@ public class SessionPersistentRegistry implements SessionRegistry { // Get the maximum number of user sessions public int getMaxUserSessions() { - if (runningEE) { - return Integer.MAX_VALUE; + if (loginEnabled) { + if (runningEE) { + return 3; + } + return Integer.MAX_VALUE; // (3) } - return 3; + return Integer.MAX_VALUE; // (10) } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java index 750cf740..f42b3a80 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionRegistryConfig.java @@ -15,7 +15,9 @@ public class SessionRegistryConfig { @Bean public SessionPersistentRegistry sessionPersistentRegistry( - SessionRepository sessionRepository, @Qualifier("runningEE") boolean runningEE) { - return new SessionPersistentRegistry(sessionRepository, runningEE); + SessionRepository sessionRepository, + @Qualifier("runningEE") boolean runningEE, + @Qualifier("loginEnabled") boolean loginEnabled) { + return new SessionPersistentRegistry(sessionRepository, runningEE, loginEnabled); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java index d5bbfefe..9459d760 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java @@ -9,9 +9,9 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.session.SessionInformation; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; @@ -20,15 +20,18 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.SessionsInterface; import stirling.software.SPDF.config.security.UserUtils; +import stirling.software.SPDF.config.security.session.CustomHttpSessionListener; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; -@RestController +@Controller @Slf4j public class SessionStatusController { @Autowired private SessionPersistentRegistry sessionPersistentRegistry; @Autowired private SessionsInterface sessionInterface; + @Autowired private CustomHttpSessionListener customHttpSessionListener; + // Returns the current session ID or 401 if no session exists @GetMapping("/session") public ResponseEntity getSession(HttpServletRequest request) { @@ -40,6 +43,35 @@ public class SessionStatusController { } } + @GetMapping("/session/invalidate/{sessionId}") + public String invalidateSession( + HttpServletRequest request, + Authentication authentication, + @PathVariable String sessionId) { + // ist ROLE_ADMIN oder session inhaber + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/login"; + } + Object principal = authentication.getPrincipal(); + String principalName = UserUtils.getUsernameFromPrincipal(principal); + if (principalName == null) { + return "redirect:/login"; + } + boolean isAdmin = + authentication.getAuthorities().stream() + .anyMatch(role -> "ROLE_ADMIN".equals(role.getAuthority())); + + boolean isOwner = + sessionPersistentRegistry.getAllSessions(principalName, false).stream() + .anyMatch(session -> session.getSessionId().equals(sessionId)); + if (isAdmin || isOwner) { + customHttpSessionListener.expireSession(sessionId, isAdmin); + return "redirect:/adminSettings?messageType=sessionInvalidated"; + } else { + return "redirect:/login"; + } + } + // Checks if the session is active and valid according to user session limits @GetMapping("/session/status") public ResponseEntity getSessionStatus(HttpServletRequest request) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index 51ab5c8b..5a2edbca 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -215,13 +215,15 @@ public class AccountWebController { @GetMapping("/adminSettings") public String showAddUserForm( HttpServletRequest request, Model model, Authentication authentication) { + String currentSessionId = request.getSession().getId(); + List allUsers = userRepository.findAll(); Iterator iterator = allUsers.iterator(); Map roleDetails = Role.getAllRoleDetails(); // Map to store session information and user activity status Map userSessions = new HashMap<>(); Map userLastRequest = new HashMap<>(); - Map userActiveSessions = new HashMap<>(); + Map> userActiveSessions = new HashMap<>(); int activeUsers = 0; int disabledUsers = 0; int maxSessions = customHttpSessionListener.getMaxApplicationSessions(); @@ -273,7 +275,7 @@ public class AccountWebController { } List sessionInformations = customHttpSessionListener.getAllSessions(user.getUsername(), false); - userActiveSessions.put(user.getUsername(), sessionInformations.size()); + userActiveSessions.put(user.getUsername(), sessionInformations); } } // Sort users by active status and last request date @@ -335,6 +337,7 @@ public class AccountWebController { } model.addAttribute("users", sortedUsers); + model.addAttribute("currentSessionId", currentSessionId); if (authentication != null) { model.addAttribute("currentUsername", authentication.getName()); } diff --git a/src/main/java/stirling/software/SPDF/model/SessionEntity.java b/src/main/java/stirling/software/SPDF/model/SessionEntity.java index f410f662..09f967c5 100644 --- a/src/main/java/stirling/software/SPDF/model/SessionEntity.java +++ b/src/main/java/stirling/software/SPDF/model/SessionEntity.java @@ -20,6 +20,8 @@ public class SessionEntity implements Serializable, SessionsModelInterface { private Date lastRequest; private boolean expired; + private Boolean adminExpired = false; + @Override public String getSessionId() { return sessionId; diff --git a/src/main/resources/templates/adminSettings.html b/src/main/resources/templates/adminSettings.html index a3f70172..8a6b4996 100644 --- a/src/main/resources/templates/adminSettings.html +++ b/src/main/resources/templates/adminSettings.html @@ -66,7 +66,7 @@
Total Users: - + Active Users: @@ -75,7 +75,8 @@ Total Sessions: - + +
@@ -107,59 +108,101 @@ th:text="#{adminUserSettings.authenticated}">Authenticated Last Request - - User Sessions - - Actions + User Sessions + Actions - - - - - - - - - + + + + + + + - - - + + + + | + + + + + 0 + + +
+ +
+ + edit + -
- -
- -
- edit - - -
- - - -
- - + +
+ + + +
+ + + + +
+ + + + + + + + + + + +
+ + + ⚠️ + + +
+ + +
+
Keine aktiven Sessions
+
+ + +