From dd2ae3d5cfc5f0c44398e237330bd061e0113fa7 Mon Sep 17 00:00:00 2001 From: Abhijeet <41686026+abhvsn@users.noreply.github.com> Date: Tue, 16 May 2023 15:42:04 +0530 Subject: [PATCH] chore: Create helper method for user session service (#23243) ## Description For security concious customers we want to enable the tenant level setting to enable single session per user. Which means if user tries to login with different browser/machine we should invalidate the existing session for the user. This PR adds the tenant level config boolean variable `enableSingleSessionPerUser` which by default will opt out of this functionality but admin user can enable this from the admin settings page. > TL;DR: Enable functionality to have a single active session per user. Fixes https://github.com/appsmithorg/appsmith/issues/22727 Corresponding EE PR: https://github.com/appsmithorg/appsmith-ee/pull/1409 ## Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update ## How Has This Been Tested? - Manual ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --- .../services/ce/SessionUserServiceCE.java | 6 ++++ .../services/ce/SessionUserServiceCEImpl.java | 33 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java index e247c1248f..136f6a26a0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java @@ -4,6 +4,8 @@ import com.appsmith.server.domains.User; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import java.util.List; + public interface SessionUserServiceCE { Mono getCurrentUser(); @@ -11,4 +13,8 @@ public interface SessionUserServiceCE { Mono refreshCurrentUser(ServerWebExchange exchange); Mono logoutAllSessions(String email); + + Mono> getSessionKeysByUserEmail(String email); + + Mono deleteSessionsByKeys(List keys); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java index 729011a576..ed67c80450 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java @@ -19,6 +19,8 @@ import org.springframework.web.server.WebSession; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; +import java.util.List; + import static org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME; @Slf4j @@ -28,6 +30,9 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { private final UserRepository userRepository; private final ReactiveRedisOperations redisOperations; + public final static String SPRING_SESSION_PATTERN = "spring:session:sessions:*"; + private final static String SESSION_ATTRIBUTE = "sessionAttr:"; + @Override public Mono getCurrentUser() { return ReactiveSecurityContextHolder.getContext() @@ -67,15 +72,22 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { @Override public Mono logoutAllSessions(String email) { + return getSessionKeysByUserEmail(email) + .flatMap(this::deleteSessionsByKeys) + .then(); + } + + @Override + public Mono> getSessionKeysByUserEmail(String email) { // This pattern string comes from calling `ReactiveRedisSessionRepository.getSessionKey("*")` private method. - return redisOperations.keys("spring:session:sessions:*") + return redisOperations.keys(SPRING_SESSION_PATTERN) .flatMap(key -> Mono.zip( Mono.just(key), // The values are maps, containing various pieces of session related information. // One of them, holds the serialized User object. We want just that. redisOperations.opsForHash().entries(key) .filter(e -> e.getValue() != null && - ("sessionAttr:" + DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME).equals(e.getKey()) + (SESSION_ATTRIBUTE + DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME).equals(e.getKey()) ) .map(e -> (User) ((SecurityContext) e.getValue()).getAuthentication().getPrincipal()) .next() @@ -84,14 +96,17 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { // Filter the ones we need to clear out. .filter(tuple -> StringUtils.equalsIgnoreCase(email, tuple.getT2().getEmail())) .map(Tuple2::getT1) - .collectList() - .flatMap(keys -> - CollectionUtils.isNullOrEmpty(keys) - ? Mono.just(0L) - : redisOperations.delete(keys.toArray(String[]::new)) + .collectList(); + + } + + @Override + public Mono deleteSessionsByKeys(List keys) { + return CollectionUtils.isNullOrEmpty(keys) + ? Mono.just(0L) + : redisOperations.delete(keys.toArray(String[]::new) ) - .doOnError(error -> log.error("Error clearing user sessions", error)) - .then(); + .doOnError(error -> log.error("Error clearing user sessions", error)); } }