diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java index 0690e6b7f6..671610b28a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java @@ -9,7 +9,9 @@ import org.springframework.data.redis.core.ReactiveRedisOperations; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession; @@ -42,4 +44,17 @@ public class RedisConfig { return new ReactiveRedisTemplate<>(factory, context); } + + // Lifted from below and turned it into a bean. Wish Spring provided it as a bean. + // RedisWebSessionConfiguration.createReactiveRedisTemplate + @Bean + ReactiveRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) { + RedisSerializer keySerializer = new StringRedisSerializer(); + RedisSerializer defaultSerializer = new JdkSerializationRedisSerializer(getClass().getClassLoader()); + RedisSerializationContext serializationContext = RedisSerializationContext + .newSerializationContext(defaultSerializer).key(keySerializer).hashKey(keySerializer) + .build(); + return new ReactiveRedisTemplate<>(factory, serializationContext); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java index bd4dfab019..74cc0fbe1e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java @@ -3,13 +3,16 @@ package com.appsmith.server.services; import com.appsmith.server.repositories.UserRepository; import com.appsmith.server.services.ce.SessionUserServiceCEImpl; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.ReactiveRedisOperations; import org.springframework.stereotype.Service; @Slf4j @Service public class SessionUserServiceImpl extends SessionUserServiceCEImpl implements SessionUserService { - public SessionUserServiceImpl(UserRepository userRepository) { - super(userRepository); + public SessionUserServiceImpl( + UserRepository userRepository, + ReactiveRedisOperations redisOperations) { + super(userRepository, redisOperations); } } 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 390ae23655..e247c1248f 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 @@ -10,4 +10,5 @@ public interface SessionUserServiceCE { Mono refreshCurrentUser(ServerWebExchange exchange); + Mono logoutAllSessions(String email); } 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 7861fcfedf..729011a576 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 @@ -3,9 +3,12 @@ package com.appsmith.server.services.ce; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.CollectionUtils; import com.appsmith.server.repositories.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.ReactiveRedisOperations; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -14,6 +17,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; import static org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME; @@ -22,6 +26,7 @@ import static org.springframework.security.web.server.context.WebSessionServerSe public class SessionUserServiceCEImpl implements SessionUserServiceCE { private final UserRepository userRepository; + private final ReactiveRedisOperations redisOperations; @Override public Mono getCurrentUser() { @@ -60,4 +65,33 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { }); } + @Override + public Mono logoutAllSessions(String email) { + // This pattern string comes from calling `ReactiveRedisSessionRepository.getSessionKey("*")` private method. + return redisOperations.keys("spring:session:sessions:*") + .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()) + ) + .map(e -> (User) ((SecurityContext) e.getValue()).getAuthentication().getPrincipal()) + .next() + )) + // Now we have tuples of session keys, and the corresponding user objects. + // 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)) + ) + .doOnError(error -> log.error("Error clearing user sessions", error)) + .then(); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java index 2278b91efd..d0a377bb65 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java @@ -60,6 +60,7 @@ import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; import javax.validation.Validator; import java.net.URLEncoder; @@ -368,6 +369,12 @@ public class UserServiceCEImpl extends BaseService ))) .flatMap(passwordResetTokenRepository::delete) .then(repository.save(userFromDb)) + .doOnSuccess(result -> + // In a separate thread, we delete all other sessions of this user. + sessionUserService.logoutAllSessions(userFromDb.getEmail()) + .subscribeOn(Schedulers.boundedElastic()) + .subscribe() + ) .thenReturn(true); })); }