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
This commit is contained in:
Abhijeet 2023-05-16 15:42:04 +05:30 committed by GitHub
parent afb763204a
commit dd2ae3d5cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 30 additions and 9 deletions

View File

@ -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<User> getCurrentUser();
@ -11,4 +13,8 @@ public interface SessionUserServiceCE {
Mono<User> refreshCurrentUser(ServerWebExchange exchange);
Mono<Void> logoutAllSessions(String email);
Mono<List<String>> getSessionKeysByUserEmail(String email);
Mono<Long> deleteSessionsByKeys(List<String> keys);
}

View File

@ -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<String, Object> redisOperations;
public final static String SPRING_SESSION_PATTERN = "spring:session:sessions:*";
private final static String SESSION_ATTRIBUTE = "sessionAttr:";
@Override
public Mono<User> getCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
@ -67,15 +72,22 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE {
@Override
public Mono<Void> logoutAllSessions(String email) {
return getSessionKeysByUserEmail(email)
.flatMap(this::deleteSessionsByKeys)
.then();
}
@Override
public Mono<List<String>> 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<Long> deleteSessionsByKeys(List<String> 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));
}
}