diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java index 2f1fbc943f..fe1aa156e2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java @@ -44,6 +44,7 @@ import static com.appsmith.server.constants.Url.PAGE_URL; import static com.appsmith.server.constants.Url.TENANT_URL; import static com.appsmith.server.constants.Url.THEME_URL; import static com.appsmith.server.constants.Url.USER_URL; +import static com.appsmith.server.constants.Url.USAGE_PULSE_URL; import static java.time.temporal.ChronoUnit.DAYS; @EnableWebFluxSecurity @@ -139,7 +140,8 @@ public class SecurityConfig { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, APPLICATION_URL + "/**"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, THEME_URL + "/**"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, ACTION_URL + "/execute"), - ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, TENANT_URL + "/current") + ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, TENANT_URL + "/current"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USAGE_PULSE_URL) ) .permitAll() .pathMatchers("/public/**", "/oauth2/**").permitAll() diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index 068a83a190..ce8c139999 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -168,4 +168,6 @@ public class FieldName { public static final String IS_FORCE_REMOVE = "forceRemove"; public static final String UNPUBLISHED_JS_LIBS_IDENTIFIER_IN_APPLICATION_CLASS = "unpublishedCustomJSLibs"; public static final String PUBLISHED_JS_LIBS_IDENTIFIER_IN_APPLICATION_CLASS = "publishedCustomJSLibs"; + public static final String ANONYMOUS_USER_ID = "anonymousUserId"; + public static final String VIEW_MODE = "viewMode"; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UsagePulseControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UsagePulseControllerCE.java index 75010250db..1320f1a17f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UsagePulseControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UsagePulseControllerCE.java @@ -1,10 +1,13 @@ package com.appsmith.server.controllers.ce; import com.appsmith.server.dtos.ResponseDTO; +import com.appsmith.server.dtos.UsagePulseDTO; import com.appsmith.server.services.UsagePulseService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseStatus; import reactor.core.publisher.Mono; @@ -15,8 +18,8 @@ public class UsagePulseControllerCE { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Mono> create() { - return service.createPulse() + public Mono> create(@RequestBody @Valid UsagePulseDTO usagePulseDTO) { + return service.createPulse(usagePulseDTO) .thenReturn(new ResponseDTO<>(HttpStatus.CREATED.value(), true, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/UsagePulse.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/UsagePulse.java index bc7098fe01..d780cfafd8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/UsagePulse.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/UsagePulse.java @@ -8,10 +8,16 @@ import org.springframework.data.mongodb.core.mapping.Document; @Getter @Setter -@AllArgsConstructor @Document public class UsagePulse extends BaseDomain { private String email; + // Hashed user email + private String user; + private String instanceId; + private String tenantId; + private Boolean viewMode; + private Boolean isAnonymousUser; + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java index 3673debe89..6f99dcd48f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java @@ -32,6 +32,8 @@ public class User extends BaseDomain implements UserDetails, OidcUser { private String email; + private String hashedEmail; + //TODO: This is deprecated in favour of groups private Set roles; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UsagePulseDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UsagePulseDTO.java new file mode 100644 index 0000000000..30fdec059a --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UsagePulseDTO.java @@ -0,0 +1,11 @@ +package com.appsmith.server.dtos; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UsagePulseDTO { + String anonymousUserId; + Boolean viewMode; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserSessionDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserSessionDTO.java index 2f30c1f71e..0f4d7d1ae7 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserSessionDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserSessionDTO.java @@ -24,6 +24,8 @@ public class UserSessionDTO { private String email; + private String hashedEmail; + private String name; private LoginSource source; @@ -65,6 +67,7 @@ public class UserSessionDTO { session.userId = user.getId(); session.email = user.getEmail(); + session.hashedEmail = user.getHashedEmail(); session.name = user.getName(); session.source = user.getSource(); session.state = user.getState(); @@ -97,6 +100,7 @@ public class UserSessionDTO { user.setId(userId); user.setEmail(email); + user.setHashedEmail(hashedEmail); user.setName(name); user.setSource(source); user.setState(state); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UsagePulseServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UsagePulseServiceImpl.java index efb3f52a5d..ea9f7006ca 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UsagePulseServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UsagePulseServiceImpl.java @@ -7,8 +7,12 @@ import org.springframework.stereotype.Service; @Service public class UsagePulseServiceImpl extends UsagePulseServiceCEImpl implements UsagePulseService { - public UsagePulseServiceImpl(UsagePulseRepository repository, SessionUserService sessionUserService) { - super(repository, sessionUserService); + public UsagePulseServiceImpl(UsagePulseRepository repository, + SessionUserService sessionUserService, + UserService userService, + TenantService tenantService, + ConfigService configService) { + super(repository, sessionUserService, userService, tenantService, configService); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCE.java index aa0aa13ba8..bdd98515b3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCE.java @@ -1,7 +1,10 @@ package com.appsmith.server.services.ce; +import com.appsmith.server.domains.UsagePulse; +import com.appsmith.server.dtos.UsagePulseDTO; import reactor.core.publisher.Mono; public interface UsagePulseServiceCE { - Mono createPulse(); + Mono createPulse(UsagePulseDTO usagePulseDTO); + Mono save(UsagePulse usagePulse); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java index 29a6112e91..b39510f5ad 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java @@ -1,9 +1,19 @@ package com.appsmith.server.services.ce; +import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.UsagePulse; +import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.UsagePulseDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.repositories.ce.UsagePulseRepositoryCE; +import com.appsmith.server.services.ConfigService; import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.TenantService; +import com.appsmith.server.services.UserService; import lombok.RequiredArgsConstructor; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; import reactor.core.publisher.Mono; @RequiredArgsConstructor @@ -13,11 +23,76 @@ public class UsagePulseServiceCEImpl implements UsagePulseServiceCE { private final SessionUserService sessionUserService; + private final UserService userService; + + private final TenantService tenantService; + + private final ConfigService configService; + + /** + * To create a usage pulse + * @param usagePulseDTO UsagePulseDTO + * @return Mono of UsagePulse + */ @Override - public Mono createPulse() { - return sessionUserService.getCurrentUser() - .flatMap(user -> repository.save(new UsagePulse(user.getEmail()))) - .then(); + public Mono createPulse(UsagePulseDTO usagePulseDTO) { + if (null == usagePulseDTO.getViewMode()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.VIEW_MODE)); + } + + UsagePulse usagePulse = new UsagePulse(); + usagePulse.setEmail(null); + usagePulse.setViewMode(usagePulseDTO.getViewMode()); + + Mono currentUserMono = sessionUserService.getCurrentUser(); + // TODO: Change to getCurrentTenantId once multi-tenancy in introduced + Mono tenantIdMono = tenantService.getDefaultTenantId(); + Mono instanceIdMono = configService.getInstanceId(); + + return Mono.zip(currentUserMono, tenantIdMono, instanceIdMono) + .flatMap(tuple -> { + User user = tuple.getT1(); + String tenantId = tuple.getT2(); + String instanceId = tuple.getT3(); + usagePulse.setTenantId(tenantId); + usagePulse.setInstanceId(instanceId); + + if (user.isAnonymous()) { + if (null == usagePulseDTO.getAnonymousUserId()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ANONYMOUS_USER_ID)); + } + usagePulse.setIsAnonymousUser(true); + usagePulse.setUser(usagePulseDTO.getAnonymousUserId()); + } + else { + usagePulse.setIsAnonymousUser(false); + if (user.getHashedEmail() == null || StringUtils.isEmpty(user.getHashedEmail())) { + String hashedEmail = DigestUtils.sha256Hex(user.getEmail()); + usagePulse.setUser(hashedEmail); + // Hashed user email is stored to user for future mapping of user and pulses + User updateUser = new User(); + updateUser.setHashedEmail(hashedEmail); + updateUser.setPasswordResetInitiated(user.getPasswordResetInitiated()); + updateUser.setSource(user.getSource()); + updateUser.setGroupIds(null); + updateUser.setPolicies(null); + + return Mono.zip(userService.update(user.getId(), updateUser),save(usagePulse)) + .map(tuple1 -> tuple1.getT2()); + } + usagePulse.setUser(user.getHashedEmail()); + } + return save(usagePulse); + }); + } + + /** + * To save usagePulse to the database + * @param usagePulse UsagePulse + * @return Mono of UsagePulse + */ + public Mono save(UsagePulse usagePulse) { + return repository.save(usagePulse); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UsagePulseServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UsagePulseServiceTest.java new file mode 100644 index 0000000000..7535641b31 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UsagePulseServiceTest.java @@ -0,0 +1,101 @@ +package com.appsmith.server.services; + +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.dtos.UsagePulseDTO; +import com.appsmith.server.exceptions.AppsmithError; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@DirtiesContext +@Slf4j +public class UsagePulseServiceTest { + + @Autowired + private UsagePulseService usagePulseService; + + /** + * To verify anonymous user usage pulses are logged properly + */ + @Test + @WithUserDetails(value = "anonymousUser") + public void test_AnonymousUserPulse_Success() { + UsagePulseDTO usagePulseDTO = new UsagePulseDTO(); + String anonymousUserId = "testAnonymousUserId"; + usagePulseDTO.setViewMode(false); + usagePulseDTO.setAnonymousUserId(anonymousUserId); + + StepVerifier.create(usagePulseService.createPulse(usagePulseDTO)) + .assertNext(usagePulse -> { + assertThat(usagePulse.getId()).isNotNull(); + assertThat(usagePulse.getEmail()).isNull(); + assertThat(usagePulse.getUser()).isEqualTo(anonymousUserId); + assertThat(usagePulse.getIsAnonymousUser()).isTrue(); + assertThat(usagePulse.getInstanceId()).isNotNull(); + assertThat(usagePulse.getTenantId()).isNotNull(); + assertThat(usagePulse.getViewMode()).isFalse(); + }) + .verifyComplete(); + } + + /** + * To verify anonymous usage pulse without anonymousUserId will fail + */ + @Test + @WithUserDetails(value = "anonymousUser") + public void test_AnonymousUserPulse_Invalid_AnonymousUserId_ThrowsException() { + UsagePulseDTO usagePulseDTO = new UsagePulseDTO(); + usagePulseDTO.setViewMode(false); + + StepVerifier.create(usagePulseService.createPulse(usagePulseDTO)) + .expectErrorMessage(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ANONYMOUS_USER_ID)) + .verify(); + } + + /** + * To verify logged in user usage pulses are logged properly + */ + @Test + @WithUserDetails(value = "api_user") + public void test_loggedInUserPulse_Success() { + UsagePulseDTO usagePulseDTO = new UsagePulseDTO(); + usagePulseDTO.setViewMode(true); + + StepVerifier.create(usagePulseService.createPulse(usagePulseDTO)) + .assertNext(usagePulse -> { + String hashedUserEmail = DigestUtils.sha256Hex("api_user"); + assertThat(usagePulse.getId()).isNotNull(); + assertThat(usagePulse.getEmail()).isNull(); + assertThat(usagePulse.getUser()).isEqualTo(hashedUserEmail); + assertThat(usagePulse.getIsAnonymousUser()).isFalse(); + assertThat(usagePulse.getInstanceId()).isNotNull(); + assertThat(usagePulse.getTenantId()).isNotNull(); + assertThat(usagePulse.getViewMode()).isTrue(); + }) + .verifyComplete(); + } + + /** + * To verify usage pulses without viewMode will fail + */ + @Test + @WithUserDetails(value = "api_user") + public void test_Invalid_ViewMode_ThrowsException() { + UsagePulseDTO usagePulseDTO = new UsagePulseDTO(); + + StepVerifier.create(usagePulseService.createPulse(usagePulseDTO)) + .expectErrorMessage(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.VIEW_MODE)) + .verify(); + } +} \ No newline at end of file