From 22479fbd3679f8c07711a5095c8d12276826a366 Mon Sep 17 00:00:00 2001 From: tkuzynow Date: Wed, 7 Dec 2022 16:30:19 +0100 Subject: [PATCH] feat: added validation --- .../controller/ExceptionHandlerAdvice.java | 8 +++- .../api/controller/TenantController.java | 3 +- .../exception/TenantValidationException.java | 3 ++ .../HttpStatusExceptionReason.java | 4 +- .../api/facade/TenantServiceFacade.java | 37 ++++++++++++++++++- .../api/service/TenantService.java | 25 +++++++------ .../api/controller/TenantControllerIT.java | 21 +++++++++++ .../MultilingualTenantTestDataBuilder.java | 10 +++++ .../validation/TenantInputSanitizerTest.java | 1 + 9 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/vi/tenantservice/api/controller/ExceptionHandlerAdvice.java b/src/main/java/com/vi/tenantservice/api/controller/ExceptionHandlerAdvice.java index 01b2fb4..b3daa69 100644 --- a/src/main/java/com/vi/tenantservice/api/controller/ExceptionHandlerAdvice.java +++ b/src/main/java/com/vi/tenantservice/api/controller/ExceptionHandlerAdvice.java @@ -2,7 +2,7 @@ import com.vi.tenantservice.api.exception.TenantAuthorisationException; import com.vi.tenantservice.api.exception.TenantValidationException; -import javax.ws.rs.BadRequestException; +import com.vi.tenantservice.api.exception.httpresponse.HttpStatusExceptionReason; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -11,6 +11,8 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import javax.ws.rs.BadRequestException; + @ControllerAdvice public class ExceptionHandlerAdvice extends ResponseEntityExceptionHandler { @@ -18,6 +20,10 @@ public class ExceptionHandlerAdvice extends ResponseEntityExceptionHandler { protected ResponseEntity handle(RuntimeException ex, WebRequest request) { var customHttpHeader = ((TenantValidationException) ex).getCustomHttpHeaders(); + HttpStatusExceptionReason statusExceptionReason = ((TenantValidationException) ex).getStatusExceptionReason(); + if (HttpStatusExceptionReason.LANGUAGE_KEY_NOT_VALID.equals(statusExceptionReason)) { + return handleExceptionInternal(ex, "", customHttpHeader, HttpStatus.BAD_REQUEST, request); + } return handleExceptionInternal(ex, "", customHttpHeader, HttpStatus.CONFLICT, request); } diff --git a/src/main/java/com/vi/tenantservice/api/controller/TenantController.java b/src/main/java/com/vi/tenantservice/api/controller/TenantController.java index 065e9e5..91db06d 100644 --- a/src/main/java/com/vi/tenantservice/api/controller/TenantController.java +++ b/src/main/java/com/vi/tenantservice/api/controller/TenantController.java @@ -2,9 +2,9 @@ import com.vi.tenantservice.api.facade.TenantServiceFacade; import com.vi.tenantservice.api.model.BasicTenantLicensingDTO; +import com.vi.tenantservice.api.model.MultilingualTenantDTO; import com.vi.tenantservice.api.model.RestrictedTenantDTO; import com.vi.tenantservice.api.model.TenantDTO; -import com.vi.tenantservice.api.model.MultilingualTenantDTO; import com.vi.tenantservice.config.security.AuthorisationService; import com.vi.tenantservice.generated.api.controller.TenantApi; import com.vi.tenantservice.generated.api.controller.TenantadminApi; @@ -71,6 +71,7 @@ public ResponseEntity createTenant(@Valid MultilingualTen @Override @PreAuthorize("hasAnyAuthority('tenant-admin', 'single-tenant-admin')") public ResponseEntity updateTenant(Long id, @Valid MultilingualTenantDTO tenantDTO) { + log.info("Updating tenant with id {} by user {} ", id, authorisationService.getUsername()); var updatedTenantDTO = tenantServiceFacade.updateTenant(id, tenantDTO); return new ResponseEntity<>(updatedTenantDTO, HttpStatus.OK); diff --git a/src/main/java/com/vi/tenantservice/api/exception/TenantValidationException.java b/src/main/java/com/vi/tenantservice/api/exception/TenantValidationException.java index 8c43329..94293fd 100644 --- a/src/main/java/com/vi/tenantservice/api/exception/TenantValidationException.java +++ b/src/main/java/com/vi/tenantservice/api/exception/TenantValidationException.java @@ -12,10 +12,13 @@ public class TenantValidationException extends RuntimeException { private final HttpHeaders customHttpHeaders; private final HttpStatus httpStatus; + private final HttpStatusExceptionReason statusExceptionReason; + public TenantValidationException(HttpStatusExceptionReason exceptionReason) { super(); this.customHttpHeaders = new CustomHttpHeader(exceptionReason) .buildHeader(); this.httpStatus = HttpStatus.CONFLICT; + this.statusExceptionReason = exceptionReason; } } diff --git a/src/main/java/com/vi/tenantservice/api/exception/httpresponse/HttpStatusExceptionReason.java b/src/main/java/com/vi/tenantservice/api/exception/httpresponse/HttpStatusExceptionReason.java index 4550bf1..e498301 100644 --- a/src/main/java/com/vi/tenantservice/api/exception/httpresponse/HttpStatusExceptionReason.java +++ b/src/main/java/com/vi/tenantservice/api/exception/httpresponse/HttpStatusExceptionReason.java @@ -4,5 +4,7 @@ public enum HttpStatusExceptionReason { SUBDOMAIN_NOT_UNIQUE, NOT_ALLOWED_TO_CHANGE_SUBDOMAIN, NOT_ALLOWED_TO_CHANGE_LICENSING, - NOT_ALLOWED_TO_CHANGE_SETTING + NOT_ALLOWED_TO_CHANGE_SETTING, + + LANGUAGE_KEY_NOT_VALID; } diff --git a/src/main/java/com/vi/tenantservice/api/facade/TenantServiceFacade.java b/src/main/java/com/vi/tenantservice/api/facade/TenantServiceFacade.java index 9d94754..54c0c21 100644 --- a/src/main/java/com/vi/tenantservice/api/facade/TenantServiceFacade.java +++ b/src/main/java/com/vi/tenantservice/api/facade/TenantServiceFacade.java @@ -1,13 +1,16 @@ package com.vi.tenantservice.api.facade; +import com.google.common.collect.Lists; import com.vi.tenantservice.api.converter.TenantConverter; import com.vi.tenantservice.api.exception.TenantNotFoundException; +import com.vi.tenantservice.api.exception.TenantValidationException; +import com.vi.tenantservice.api.exception.httpresponse.HttpStatusExceptionReason; import com.vi.tenantservice.api.model.BasicTenantLicensingDTO; +import com.vi.tenantservice.api.model.MultilingualTenantDTO; import com.vi.tenantservice.api.model.RestrictedTenantDTO; import com.vi.tenantservice.api.model.TenantDTO; import com.vi.tenantservice.api.model.TenantEntity; -import com.vi.tenantservice.api.model.MultilingualTenantDTO; import com.vi.tenantservice.api.service.TenantService; import com.vi.tenantservice.api.service.TranslationService; import com.vi.tenantservice.api.validation.TenantInputSanitizer; @@ -19,7 +22,10 @@ import org.springframework.stereotype.Service; import javax.ws.rs.BadRequestException; +import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -45,17 +51,46 @@ public class TenantServiceFacade { public MultilingualTenantDTO createTenant(MultilingualTenantDTO tenantDTO) { log.info("Creating new tenant"); MultilingualTenantDTO sanitizedTenantDTO = tenantInputSanitizer.sanitize(tenantDTO); + validateTenantInput(tenantDTO); var entity = tenantConverter.toEntity(sanitizedTenantDTO); return tenantConverter.toMultilingualDTO(tenantService.create(entity)); } public MultilingualTenantDTO updateTenant(Long id, MultilingualTenantDTO tenantDTO) { tenantFacadeAuthorisationService.assertUserIsAuthorizedToAccessTenant(id); + validateTenantInput(tenantDTO); MultilingualTenantDTO sanitizedTenantDTO = tenantInputSanitizer.sanitize(tenantDTO); log.info("Attempting to update tenant with id {}", id); return updateWithSanitizedInput(id, sanitizedTenantDTO); } + private void validateTenantInput(MultilingualTenantDTO tenantDTO) { + var isoCountries = Arrays.stream(Locale.getISOCountries()).collect(Collectors.toList()); + validateContent(tenantDTO, isoCountries); + } + + private void validateContent(MultilingualTenantDTO tenantDTO, List isoCountries) { + if (tenantDTO.getContent() != null) { + validateTranslationKeys(isoCountries, getLanguageUppercaseKeys(tenantDTO.getContent().getImpressum())); + validateTranslationKeys(isoCountries, getLanguageUppercaseKeys(tenantDTO.getContent().getPrivacy())); + validateTranslationKeys(isoCountries, getLanguageUppercaseKeys(tenantDTO.getContent().getTermsAndConditions())); + validateTranslationKeys(isoCountries, getLanguageUppercaseKeys(tenantDTO.getContent().getClaim())); + } + } + + private void validateTranslationKeys(List isoCountries, List keys) { + if (!keys.isEmpty() && !isoCountries.containsAll(keys)) { + throw new TenantValidationException(HttpStatusExceptionReason.LANGUAGE_KEY_NOT_VALID); + } + } + + private static List getLanguageUppercaseKeys(Map translatedMap) { + if (translatedMap == null) { + return Lists.newArrayList(); + } + return translatedMap.keySet().stream().map(s -> s.toUpperCase()).collect(Collectors.toList()); + } + private MultilingualTenantDTO updateWithSanitizedInput(Long id, MultilingualTenantDTO sanitizedTenantDTO) { var tenantById = tenantService.findTenantById(id); if (tenantById.isPresent()) { diff --git a/src/main/java/com/vi/tenantservice/api/service/TenantService.java b/src/main/java/com/vi/tenantservice/api/service/TenantService.java index f2d754c..2322972 100644 --- a/src/main/java/com/vi/tenantservice/api/service/TenantService.java +++ b/src/main/java/com/vi/tenantservice/api/service/TenantService.java @@ -1,20 +1,21 @@ package com.vi.tenantservice.api.service; -import static com.vi.tenantservice.api.exception.httpresponse.HttpStatusExceptionReason.SUBDOMAIN_NOT_UNIQUE; - import com.vi.tenantservice.api.exception.TenantValidationException; import com.vi.tenantservice.api.model.TenantEntity; import com.vi.tenantservice.api.repository.TenantRepository; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.List; -import java.util.Optional; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; + +import static com.vi.tenantservice.api.exception.httpresponse.HttpStatusExceptionReason.SUBDOMAIN_NOT_UNIQUE; + @Service @Slf4j @RequiredArgsConstructor @@ -27,9 +28,7 @@ public class TenantService { public TenantEntity create(TenantEntity tenantEntity) { - if (!multitenancyWithSingleDomain) { - validateTenantSubdomainDoesNotExist(tenantEntity); - } + validateTenant(tenantEntity); setCreateAndUpdateDate(tenantEntity); return tenantRepository.save(tenantEntity); } @@ -51,11 +50,15 @@ private boolean tenantWithSuchSubdomainAlreadyExists(TenantEntity tenantEntity, } public TenantEntity update(TenantEntity tenantEntity) { + validateTenant(tenantEntity); + tenantEntity.setUpdateDate(LocalDateTime.now(ZoneOffset.UTC)); + return tenantRepository.save(tenantEntity); + } + + private void validateTenant(TenantEntity tenantEntity) { if (!multitenancyWithSingleDomain) { validateTenantSubdomainDoesNotExist(tenantEntity); } - tenantEntity.setUpdateDate(LocalDateTime.now(ZoneOffset.UTC)); - return tenantRepository.save(tenantEntity); } public Optional findTenantById(Long id) { diff --git a/src/test/java/com/vi/tenantservice/api/controller/TenantControllerIT.java b/src/test/java/com/vi/tenantservice/api/controller/TenantControllerIT.java index 6fb1e1c..784ba72 100644 --- a/src/test/java/com/vi/tenantservice/api/controller/TenantControllerIT.java +++ b/src/test/java/com/vi/tenantservice/api/controller/TenantControllerIT.java @@ -331,6 +331,20 @@ void updateTenant_Should_sanitizeInput_When_calledWithExistingTenantIdAndForTena .andExpect(jsonPath("$.content.claim['de']").value("claim")); } + @Test + void updateTenant_Should_throwValidationException_When_calledWithExistingTenantIdAndForTenantAdminAuthorityButInvalidLanguageCode() + throws Exception { + String jsonRequest = prepareRequestWithInvalidLanguageContent(); + + AuthenticationMockBuilder builder = new AuthenticationMockBuilder(); + mockMvc.perform(put(EXISTING_TENANT_VIA_ADMIN) + .with(authentication(builder.withAuthority(TENANT_ADMIN.getValue()).build())) + .contentType(APPLICATION_JSON) + .content(jsonRequest) + .contentType(APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + private String prepareRequestWithInvalidScriptContent() { return multilingualTenantTestDataBuilder.withId(1L).withName(appendMalciousScript("name")) .withSubdomain(appendMalciousScript("subdomain")) @@ -338,6 +352,13 @@ private String prepareRequestWithInvalidScriptContent() { .jsonify(); } + private String prepareRequestWithInvalidLanguageContent() { + return multilingualTenantTestDataBuilder.withId(1L).withName(appendMalciousScript("name")) + .withSubdomain(appendMalciousScript("subdomain")) + .withTranslatedImpressum("abc", "impressum") + .jsonify(); + } + private String appendMalciousScript(String content) { return content + SCRIPT_CONTENT; } diff --git a/src/test/java/com/vi/tenantservice/api/util/MultilingualTenantTestDataBuilder.java b/src/test/java/com/vi/tenantservice/api/util/MultilingualTenantTestDataBuilder.java index 8d23e76..cdfbcaa 100644 --- a/src/test/java/com/vi/tenantservice/api/util/MultilingualTenantTestDataBuilder.java +++ b/src/test/java/com/vi/tenantservice/api/util/MultilingualTenantTestDataBuilder.java @@ -150,4 +150,14 @@ public MultilingualTenantTestDataBuilder withContent(String impressum, String cl tenantMultilingualDTO.setContent(content); return this; } + + public MultilingualTenantTestDataBuilder withTranslatedImpressum(String language, String impressum) { + MultilingualContent content = new MultilingualContent(); + var translatedMap = new HashMap(); + translatedMap.put(language, impressum); + content.setImpressum(translatedMap); + content.setClaim(defaultTranslations("")); + tenantMultilingualDTO.setContent(content); + return this; + } } diff --git a/src/test/java/com/vi/tenantservice/api/validation/TenantInputSanitizerTest.java b/src/test/java/com/vi/tenantservice/api/validation/TenantInputSanitizerTest.java index 60e2a86..cd40789 100644 --- a/src/test/java/com/vi/tenantservice/api/validation/TenantInputSanitizerTest.java +++ b/src/test/java/com/vi/tenantservice/api/validation/TenantInputSanitizerTest.java @@ -8,6 +8,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; + import java.util.HashMap; import java.util.Map;