chore: Updated the usage pulse format (#19565)

## Description

The usage pulse format is updated

Fixes https://github.com/appsmithorg/cloud-services/issues/148

## Type of change

- Breaking change (fix or feature that would cause existing
functionality to not work as expected)

## How Has This Been Tested?

- Manual
- JUnit

## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] 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:
Vishnu Gp 2023-01-09 10:47:34 +05:30 committed by GitHub
parent cb39318903
commit 80f23a5af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 11 deletions

View File

@ -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()

View File

@ -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";
}

View File

@ -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<ResponseDTO<Boolean>> create() {
return service.createPulse()
public Mono<ResponseDTO<Boolean>> create(@RequestBody @Valid UsagePulseDTO usagePulseDTO) {
return service.createPulse(usagePulseDTO)
.thenReturn(new ResponseDTO<>(HttpStatus.CREATED.value(), true, null));
}

View File

@ -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;
}

View File

@ -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<Role> roles;

View File

@ -0,0 +1,11 @@
package com.appsmith.server.dtos;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UsagePulseDTO {
String anonymousUserId;
Boolean viewMode;
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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<Void> createPulse();
Mono<UsagePulse> createPulse(UsagePulseDTO usagePulseDTO);
Mono<UsagePulse> save(UsagePulse usagePulse);
}

View File

@ -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<Void> createPulse() {
return sessionUserService.getCurrentUser()
.flatMap(user -> repository.save(new UsagePulse(user.getEmail())))
.then();
public Mono<UsagePulse> 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<User> currentUserMono = sessionUserService.getCurrentUser();
// TODO: Change to getCurrentTenantId once multi-tenancy in introduced
Mono<String> tenantIdMono = tenantService.getDefaultTenantId();
Mono<String> 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<UsagePulse> save(UsagePulse usagePulse) {
return repository.save(usagePulse);
}
}

View File

@ -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();
}
}