From 1b2c3fc0a3630b2e2ed0449243d0e195f556375d Mon Sep 17 00:00:00 2001 From: Sebastian-Ion TINCU <Sebastian-Ion.TINCU@ext.ec.europa.eu> Date: Thu, 29 Feb 2024 12:12:54 +0100 Subject: [PATCH] EDELIVERY-11816 Resources Search Filter Does Not Support Colon Caracter Add annotation to decode URI-encoded filter values. --- smp-webapp/pom.xml | 12 +++ .../edelivery/smp/config/SMPWebAppConfig.java | 12 ++- .../ec/edelivery/smp/filter/Filter.java | 34 +++++++++ .../edelivery/smp/filter/FilterHandler.java | 74 +++++++++++++++++++ .../smp/ui/edit/DomainEditController.java | 3 +- .../smp/ui/edit/GroupEditController.java | 3 +- .../smp/ui/edit/ResourceEditController.java | 5 +- .../smp/ui/external/UserController.java | 3 +- .../smp/ui/internal/UserAdminController.java | 3 +- .../smp/ui/edit/ResourceEditControllerIT.java | 38 +++++++++- 10 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/Filter.java create mode 100644 smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/FilterHandler.java diff --git a/smp-webapp/pom.xml b/smp-webapp/pom.xml index 976fefb17..a89c39ba0 100644 --- a/smp-webapp/pom.xml +++ b/smp-webapp/pom.xml @@ -125,6 +125,18 @@ <artifactId>mysql-connector-j</artifactId> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <exclusions> + <exclusion> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-logging</artifactId> + </exclusion> + </exclusions> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SMPWebAppConfig.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SMPWebAppConfig.java index c5bbaedab..4dd672e7f 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SMPWebAppConfig.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SMPWebAppConfig.java @@ -23,12 +23,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import eu.europa.ec.edelivery.smp.filter.FilterHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.PropertySource; +import org.springframework.format.FormatterRegistry; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.*; @@ -92,6 +94,7 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = SMPWebAppConfig.class)}) @PropertySource(value = "classpath:/application.properties", ignoreResourceNotFound = true) public class SMPWebAppConfig implements WebMvcConfigurer { + private static final Logger LOG = LoggerFactory.getLogger(SMPWebAppConfig.class); @Override @@ -103,7 +106,6 @@ public class SMPWebAppConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.setOrder(HIGHEST_PRECEDENCE) .addResourceHandler("/index.html", "/favicon.png", "/favicon.ico").addResourceLocations("/html/"); @@ -127,7 +129,7 @@ public class SMPWebAppConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { // Configure Object Mapper with format date as: "2021-12-01T14:52:00Z" - LOG.debug("Register MappingJackson2HttpMessageConverter."); + LOG.debug("Register MappingJackson2HttpMessageConverter"); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = JsonMapper.builder() .findAndAddModules() @@ -143,4 +145,10 @@ public class SMPWebAppConfig implements WebMvcConfigurer { converters.add(0, converter); } + + @Override + public void addFormatters(FormatterRegistry registry) { + LOG.debug("Register FilterHandler"); + registry.addFormatterForFieldAnnotation(new FilterHandler()); + } } diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/Filter.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/Filter.java new file mode 100644 index 000000000..f3c71839e --- /dev/null +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/Filter.java @@ -0,0 +1,34 @@ +/*- + * #START_LICENSE# + * smp-webapp + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.filter; + +import java.lang.annotation.*; + +/** + * Annotation used to mark a filter provided as an HTTP request parameters which needs its value to be URI-decoded. + * + * @author Sebastian-Ion TINCU + * @since 5.1 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Filter { + +} diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/FilterHandler.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/FilterHandler.java new file mode 100644 index 000000000..71a167a5f --- /dev/null +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/filter/FilterHandler.java @@ -0,0 +1,74 @@ +/*- + * #START_LICENSE# + * smp-webapp + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.filter; + +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.Printer; + +import java.net.URLDecoder; +import java.text.ParseException; +import java.util.*; + +/** + * A handler that parses filter values by decoding their tokens that have been URI-encoded in the front end. + * + * @author Sebastian-Ion TINCU + * @since 5.1 + */ +public class FilterHandler implements AnnotationFormatterFactory<Filter> { + + private static final Set<Class<?>> FIELD_TYPES; + + static { + Set<Class<?>> fieldTypes = new HashSet<>(); + fieldTypes.add(String.class); + FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); + } + + @Override + public Set<Class<?>> getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public Printer<?> getPrinter(Filter annotation, Class<?> fieldType) { + return getFormatter(annotation); + } + + @Override + public Parser<?> getParser(Filter annotation, Class<?> fieldType) { + return getFormatter(annotation); + } + + private Formatter<String> getFormatter(Filter annotation) { + return new Formatter<String>() { + @Override + public String print(String object, Locale locale) { + return object; + } + + @Override + public String parse(String encoded, Locale locale) throws ParseException { + return URLDecoder.decode(encoded); + } + }; + } +} diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/DomainEditController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/DomainEditController.java index 2d4962269..45583d315 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/DomainEditController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/DomainEditController.java @@ -26,6 +26,7 @@ import eu.europa.ec.edelivery.smp.data.ui.ResourceDefinitionRO; import eu.europa.ec.edelivery.smp.data.ui.ServiceResult; import eu.europa.ec.edelivery.smp.exceptions.ErrorCode; import eu.europa.ec.edelivery.smp.exceptions.SMPRuntimeException; +import eu.europa.ec.edelivery.smp.filter.Filter; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UIDomainPublicService; @@ -93,7 +94,7 @@ public class DomainEditController { @PathVariable(PATH_PARAM_ENC_DOMAIN_ID) String domainEncId, @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { logAdminAccess("getDomainMemberList"); LOG.info("Search for domain members with filter [{}], paging: [{}/{}], user: {}", filter, page, pageSize, userEncId); Long domainId = SessionSecurityUtils.decryptEntityId(domainEncId); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/GroupEditController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/GroupEditController.java index 83f8c3375..8dd9011a3 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/GroupEditController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/GroupEditController.java @@ -25,6 +25,7 @@ import eu.europa.ec.edelivery.smp.data.ui.MemberRO; import eu.europa.ec.edelivery.smp.data.ui.ServiceResult; import eu.europa.ec.edelivery.smp.exceptions.ErrorCode; import eu.europa.ec.edelivery.smp.exceptions.SMPRuntimeException; +import eu.europa.ec.edelivery.smp.filter.Filter; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UIGroupPublicService; @@ -145,7 +146,7 @@ public class GroupEditController { @PathVariable(PATH_PARAM_ENC_GROUP_ID) String groupEncId, @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { LOG.info("Search for group members with filter [{}], paging: [{}/{}], user: {}", filter, page, pageSize, userEncId); Long groupId = SessionSecurityUtils.decryptEntityId(groupEncId); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditController.java index e16d71fcf..4b4e23c47 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditController.java @@ -25,6 +25,7 @@ import eu.europa.ec.edelivery.smp.data.ui.ResourceRO; import eu.europa.ec.edelivery.smp.data.ui.ServiceResult; import eu.europa.ec.edelivery.smp.exceptions.ErrorCode; import eu.europa.ec.edelivery.smp.exceptions.SMPRuntimeException; +import eu.europa.ec.edelivery.smp.filter.Filter; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UIResourceService; @@ -76,7 +77,7 @@ public class ResourceEditController { @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, @RequestParam(value = PARAM_NAME_TYPE, defaultValue = "", required = false) String forRole, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { logAdminAccess("getResourcesForGroup and type: " + forRole); Long groupId = SessionSecurityUtils.decryptEntityId(groupEncId); Long userId = SessionSecurityUtils.decryptEntityId(userEncId); @@ -146,7 +147,7 @@ public class ResourceEditController { @PathVariable(PATH_PARAM_ENC_RESOURCE_ID) String resourceEncId, @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { LOG.info("Search for group members with filter [{}], paging: [{}/{}], user: {}", filter, page, pageSize, userEncId); Long groupId = SessionSecurityUtils.decryptEntityId(groupEncId); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserController.java index b794ca42a..9213164a1 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserController.java @@ -25,6 +25,7 @@ import eu.europa.ec.edelivery.smp.data.enums.CredentialTargetType; import eu.europa.ec.edelivery.smp.data.enums.CredentialType; import eu.europa.ec.edelivery.smp.data.model.user.DBUser; import eu.europa.ec.edelivery.smp.data.ui.*; +import eu.europa.ec.edelivery.smp.filter.Filter; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UIAlertService; @@ -78,7 +79,7 @@ public class UserController { @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userId)") @GetMapping(path = "/{user-id}/search", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public List<SearchUserRO> lookupUsers(@PathVariable(PATH_PARAM_ENC_USER_ID) String userId, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { Long entityId = decryptEntityId(userId); LOG.info("Validating the password of the currently logged in user:[{}] with id:[{}] ", userId, entityId); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminController.java index eead0c772..6699cf9e9 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminController.java @@ -23,6 +23,7 @@ import eu.europa.ec.edelivery.smp.auth.SMPUserDetails; import eu.europa.ec.edelivery.smp.data.model.user.DBUser; import eu.europa.ec.edelivery.smp.data.ui.*; import eu.europa.ec.edelivery.smp.data.ui.auth.SMPAuthority; +import eu.europa.ec.edelivery.smp.filter.Filter; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UITruststoreService; @@ -84,7 +85,7 @@ public class UserAdminController { @PathVariable(PATH_PARAM_ENC_USER_ID) String userEncId, @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, - @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) @Filter String filter) { LOG.info("Search user with filter [{}], paging: [{}/{}], user: {}",filter, page, pageSize, userEncId); return uiUserService.searchUsers(page, pageSize, filter); diff --git a/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditControllerIT.java b/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditControllerIT.java index bdb26e338..0f614c20d 100644 --- a/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditControllerIT.java +++ b/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/ui/edit/ResourceEditControllerIT.java @@ -21,24 +21,30 @@ package eu.europa.ec.edelivery.smp.ui.edit; import eu.europa.ec.edelivery.smp.data.enums.VisibilityType; import eu.europa.ec.edelivery.smp.data.ui.*; import eu.europa.ec.edelivery.smp.services.ui.UIResourceSearchService; +import eu.europa.ec.edelivery.smp.services.ui.UIResourceService; import eu.europa.ec.edelivery.smp.services.ui.filters.ResourceFilter; import eu.europa.ec.edelivery.smp.ui.AbstractControllerTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MvcResult; import java.io.IOException; +import java.net.URLEncoder; import java.util.List; import java.util.UUID; import static eu.europa.ec.edelivery.smp.test.testutils.MockMvcUtils.*; import static eu.europa.ec.edelivery.smp.ui.ResourceConstants.*; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -52,10 +58,16 @@ public class ResourceEditControllerIT extends AbstractControllerTest { @Autowired protected UIResourceSearchService uiResourceSearchService; + @SpyBean + private UIResourceService uiResourceService; + @Autowired + private ResourceEditController resourceEditController; @BeforeEach public void setup() throws IOException { super.setup(); + + ReflectionTestUtils.setField(resourceEditController, "uiResourceService", uiResourceService); } // test must match the webapp_integration_test_data.sql file! @@ -290,8 +302,32 @@ public class ResourceEditControllerIT extends AbstractControllerTest { assertEquals(member.getRoleType(), response.getRoleType()); } - public int getResourceCount() { return uiResourceSearchService.getTableList(-1, -1, null, null, new ResourceFilter()).getCount().intValue(); } + + @Test + public void testFilter() throws Exception { + // given when + final String filter = ":%#^&$-_=asd.<>/\\"; + String filterParam = URLEncoder.encode(filter); + MockHttpSession session = loginWithSystemAdmin(mvc); + UserRO userRO = getLoggedUserData(mvc, session); + List<DomainRO> domainsForUser = geUserDomainsForRole(mvc, session, userRO, null); + assertEquals(1, domainsForUser.size()); + DomainRO domainRO = domainsForUser.get(0); + GroupRO groupRO = addGroupToDomain(session, domainRO, userRO); + + // when + mvc.perform(get(PATH, userRO.getUserId(), + domainRO.getDomainId(), + groupRO.getGroupId()) + .session(session) + .param(PARAM_PAGINATION_FILTER, filterParam) + .with(csrf())) + .andExpect(status().isOk()).andReturn(); + + //then + Mockito.verify(uiResourceService).getGroupResources(anyLong(), anyInt(), anyInt(), eq(filter)); + } } -- GitLab