diff options
author | Alain Kermis <alain.kermis@sonarsource.com> | 2023-12-06 16:46:21 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-12-19 20:02:55 +0000 |
commit | a7a5ec9cecb3e5146094b50af61f02ab067b455c (patch) | |
tree | ecd9e598a2b47b3030ae7470c5fb1301504c0739 /server/sonar-webserver-webapi-v2 | |
parent | 6a2f77f0e34e09ddd7202e92e685948229570f3f (diff) | |
download | sonarqube-a7a5ec9cecb3e5146094b50af61f02ab067b455c.tar.gz sonarqube-a7a5ec9cecb3e5146094b50af61f02ab067b455c.zip |
SONAR-21228 Log deprecated API usage for API V2
Diffstat (limited to 'server/sonar-webserver-webapi-v2')
6 files changed, 369 insertions, 1 deletions
diff --git a/server/sonar-webserver-webapi-v2/build.gradle b/server/sonar-webserver-webapi-v2/build.gradle index 93b98d477a2..14af058a25a 100644 --- a/server/sonar-webserver-webapi-v2/build.gradle +++ b/server/sonar-webserver-webapi-v2/build.gradle @@ -18,11 +18,15 @@ dependencies { testImplementation 'javax.servlet:javax.servlet-api' testImplementation 'org.mockito:mockito-core' testImplementation 'org.skyscreamer:jsonassert:1.5.1' + testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures' + testImplementation 'com.tngtech.java:junit-dataprovider' testImplementation project(':sonar-testing-harness') testImplementation testFixtures(project(':server:sonar-server-common')) testImplementation testFixtures(project(':server:sonar-webserver-auth')) testFixturesApi 'org.springframework:spring-test' + + testRuntimeOnly 'org.apache.logging.log4j:log4j-core' } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DeprecatedHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DeprecatedHandler.java new file mode 100644 index 00000000000..94701a2a101 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/DeprecatedHandler.java @@ -0,0 +1,137 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.v2.common; + +import java.lang.reflect.Field; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.user.UserSession; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import static org.sonar.process.logging.LogbackHelper.DEPRECATION_LOGGER_NAME; + +/** + * Interceptor that logs deprecation warnings for deprecated web services and parameters that are used. The only thing this handler will not + * log is used deprecated fields of {@link org.springframework.web.bind.annotation.RequestBody()}. This needs to be covered at some point. + * </p> + */ +@Component +public class DeprecatedHandler implements HandlerInterceptor { + + private static final Logger LOGGER = LoggerFactory.getLogger(DEPRECATION_LOGGER_NAME); + + private final UserSession userSession; + + public DeprecatedHandler(UserSession userSession) { + this.userSession = userSession; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (handler instanceof HandlerMethod handlerMethod) { + preHandle(handlerMethod, request); + } else { + LOGGER.debug("Handler is not a HandlerMethod, skipping deprecated check."); + } + + return true; + } + + private void preHandle(HandlerMethod handlerMethod, HttpServletRequest request) { + Level logLevel = getLogLevel(); + Deprecated deprecatedEndpoint = handlerMethod.getMethodAnnotation(Deprecated.class); + if (deprecatedEndpoint != null) { + logDeprecatedWebServiceMessage(logLevel, deprecatedEndpoint.since()); + } + + handleParams(handlerMethod, logLevel, request); + } + + private static void handleParams(HandlerMethod handlerMethod, Level logLevel, HttpServletRequest request) { + for (MethodParameter param : handlerMethod.getMethodParameters()) { + if (isV2ParameterObject(param)) { + checkDeprecatedFields(param.getParameterType(), logLevel, request); + } else if (isUsedDeprecatedRequestParam(request, param)) { + String paramName = param.getParameterAnnotation(RequestParam.class).name(); + String deprecatedSince = param.getParameterAnnotation(Deprecated.class).since(); + logDeprecatedParamMessage(logLevel, paramName, deprecatedSince); + } + } + } + + private static void checkDeprecatedFields(Class<?> clazz, Level logLevel, HttpServletRequest request) { + for (Field field : clazz.getDeclaredFields()) { + if (isUsedDeprecatedField(request, field)) { + String deprecatedSince = field.getAnnotation(Deprecated.class).since(); + logDeprecatedParamMessage(logLevel, field.getName(), deprecatedSince); + } + + if (isApiV2Param(field.getType())) { + checkDeprecatedFields(field.getType(), logLevel, request); + } + } + } + + private Level getLogLevel() { + return isAuthenticatedBrowserSessionOrUnauthenticatedUser() ? Level.DEBUG : Level.WARN; + } + + private boolean isAuthenticatedBrowserSessionOrUnauthenticatedUser() { + return userSession instanceof ThreadLocalUserSession threadLocalUserSession + && (threadLocalUserSession.hasSession() + && (!userSession.isLoggedIn() || userSession.isAuthenticatedBrowserSession())); + } + + private static void logDeprecatedWebServiceMessage(Level logLevel, String deprecatedSince) { + LOGGER.atLevel(logLevel).log("Web service is deprecated since {} and will be removed in a future version.", deprecatedSince); + } + + private static void logDeprecatedParamMessage(Level logLevel, String field, String deprecatedSince) { + LOGGER.atLevel(logLevel).log("Parameter '{}' is deprecated since {} and will be removed in a future version.", field, deprecatedSince); + } + + private static boolean isUsedDeprecatedRequestParam(HttpServletRequest request, MethodParameter param) { + return param.hasParameterAnnotation(Deprecated.class) && + param.hasParameterAnnotation(RequestParam.class) && + request.getParameter(param.getParameterAnnotation(RequestParam.class).name()) != null; + } + + private static boolean isUsedDeprecatedField(HttpServletRequest request, Field field) { + return field.getAnnotation(Deprecated.class) != null && request.getParameter(field.getName()) != null; + } + + private static boolean isV2ParameterObject(MethodParameter param) { + return param.hasParameterAnnotation(ParameterObject.class) && isApiV2Param(param.getParameterType()); + } + + private static boolean isApiV2Param(Class<?> clazz) { + return clazz.getTypeName().startsWith("org.sonar.server.v2"); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java index 0cb87b69044..3df7ae8ebe1 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java @@ -52,9 +52,11 @@ import org.sonar.server.v2.api.system.controller.LivenessController; import org.sonar.server.v2.api.user.controller.DefaultUserController; import org.sonar.server.v2.api.user.controller.UserController; import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator; +import org.sonar.server.v2.common.DeprecatedHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Configuration @Import(CommonWebConfig.class) @@ -112,5 +114,11 @@ public class PlatformLevel4WebConfig { return new DefaultRuleController(userSession, ruleService, ruleRestResponseGenerator); } + @Bean + public RequestMappingHandlerMapping requestMappingHandlerMapping(UserSession userSession) { + RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); + handlerMapping.setInterceptors(new DeprecatedHandler(userSession)); + return handlerMapping; + } } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/DeprecatedHandlerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/DeprecatedHandlerTest.java new file mode 100644 index 00000000000..f8b24ed8b0e --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/DeprecatedHandlerTest.java @@ -0,0 +1,206 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.v2.common; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTester; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.v2.api.ControllerTester; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; + +@RunWith(DataProviderRunner.class) +public class DeprecatedHandlerTest { + + private static final String DEPRECATED_VERSION = "10.0"; + private static final String DEPRECATED_WEB_SERVICE = "Web service is deprecated since %s and will be removed in a future version."; + private static final String DEPRECATED_PARAM = "Parameter '%s' is deprecated since %s and will be removed in a future version."; + + private final ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class); + private final MockMvc mockMvc = ControllerTester.getMockMvcWithHandlerInterceptors(List.of(new DeprecatedHandler(userSession)), new TestController()); + + @Rule + public LogTester logTester = new LogTester().setLevel(Level.DEBUG); + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsDeprecatedGetMethod_shouldShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-deprecated-endpoint"); + + assertThat(logTester.logs(expectedLogLevel)).contains(DEPRECATED_WEB_SERVICE.formatted(DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsNotDeprecatedGetMethod_shouldNotShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-not-deprecated-endpoint"); + + assertThat(logTester.logs(expectedLogLevel)).doesNotContain(DEPRECATED_WEB_SERVICE.formatted(DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsGetMethodWithUsedDeprecatedParamObjectField_shouldShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-deprecated-param-obj?deprecatedField=foo¬DeprecatedField=bar"); + + assertThat(logTester.logs(expectedLogLevel)).contains(DEPRECATED_PARAM.formatted("deprecatedField", DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsGetMethodWithUnusedDeprecatedParam_shouldNotShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-deprecated-param-obj?notDeprecatedParam=bar"); + + assertThat(logTester.logs(expectedLogLevel)).doesNotContain(DEPRECATED_PARAM.formatted("notDeprecatedParam", DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsGetMethodWithUsedDeprecatedSimpleParam_shouldShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-deprecated-param?deprecatedParam=foo"); + + assertThat(logTester.logs(expectedLogLevel)).contains(DEPRECATED_PARAM.formatted("deprecatedParam", DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsGetMethodWithUnusedDeprecatedSimpleParam_shouldNotShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(GET, sessionData, "/test/get-deprecated-param?notDeprecatedField=bar"); + + assertThat(logTester.logs(expectedLogLevel)).doesNotContain(DEPRECATED_PARAM.formatted("notDeprecatedField", DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsDeprecatedPostMethod_shouldShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(POST, sessionData, "/test/post-deprecated-endpoint"); + + assertThat(logTester.logs(expectedLogLevel)).contains(DEPRECATED_WEB_SERVICE.formatted(DEPRECATED_VERSION)); + } + + @Test + @UseDataProvider("userSessions") + public void preHandle_whenHandlerContainsNotDeprecatedPostMethod_shouldNotShowDeprecatedLogs(UserSessionData sessionData, Level expectedLogLevel) throws Exception { + performRequest(POST, sessionData, "/test/post-not-deprecated-endpoint"); + + assertThat(logTester.logs(expectedLogLevel)).doesNotContain(DEPRECATED_WEB_SERVICE.formatted(DEPRECATED_VERSION)); + } + + @Test + public void preHandle_whenNotHandlerMethod_shouldLogDebugMessage() { + DeprecatedHandler handler = new DeprecatedHandler(userSession); + + boolean result = handler.preHandle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), "Not handler"); + + assertThat(result).isTrue(); + assertThat(logTester.logs(Level.DEBUG)).contains("Handler is not a HandlerMethod, skipping deprecated check."); + } + + private void performRequest(HttpMethod method, UserSessionData sessionData, String endpoint) throws Exception { + when(userSession.hasSession()).thenReturn(true); + when(userSession.isLoggedIn()).thenReturn(sessionData.isLoggedIn()); + when(userSession.isAuthenticatedBrowserSession()).thenReturn(sessionData.isAuthenticatedBrowserSession()); + + mockMvc.perform(request(method, endpoint)); + } + + @DataProvider + public static Object[][] userSessions() { + return new Object[][] { + {new UserSessionData(false, false), Level.DEBUG }, + {new UserSessionData(false, true), Level.DEBUG}, + {new UserSessionData(true, false), Level.WARN}, + {new UserSessionData(true, true), Level.DEBUG} + }; + } + + @RequestMapping("test") + @RestController + private static class TestController { + @Deprecated(since = DEPRECATED_VERSION) + @ResponseStatus(HttpStatus.NO_CONTENT) + @GetMapping("/get-deprecated-endpoint") + void deprecatedGet() { + } + + @GetMapping("/get-not-deprecated-endpoint") + @ResponseStatus(HttpStatus.NO_CONTENT) + void notDeprecatedGet() { + } + + @GetMapping("/get-deprecated-param") + @ResponseStatus(HttpStatus.NO_CONTENT) + void deprecatedGetParam(@Deprecated(since = DEPRECATED_VERSION) @RequestParam(name = "deprecatedParam") String deprecatedParam, + @RequestParam(name = "notDeprecatedParam") String notDeprecatedParam) { + } + + @GetMapping("/get-deprecated-param-obj") + @ResponseStatus(HttpStatus.NO_CONTENT) + void deprecatedGetParam(@ParameterObject GetRequest request) { + } + + @Deprecated(since = DEPRECATED_VERSION) + @PostMapping("/post-deprecated-endpoint") + @ResponseStatus(HttpStatus.NO_CONTENT) + void deprecatedPost(@RequestBody Object request) { + } + + @PostMapping("/post-not-deprecated-endpoint") + @ResponseStatus(HttpStatus.NO_CONTENT) + void notDeprecatedPost(@RequestBody Object request) { + } + } + + private static class GetRequest { + @Deprecated(since = DEPRECATED_VERSION) + private String deprecatedField; + private String notDeprecatedField; + } + + private record UserSessionData(boolean isLoggedIn, boolean isAuthenticatedBrowserSession) { + } + +} + diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java index b295b12d3b2..2f5bd83f1ce 100644 --- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/config/CommonWebConfigTest.java @@ -27,7 +27,6 @@ import org.springframework.web.util.UrlPathHelper; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class CommonWebConfigTest { diff --git a/server/sonar-webserver-webapi-v2/src/testFixtures/java/org/sonar/server/v2/api/ControllerTester.java b/server/sonar-webserver-webapi-v2/src/testFixtures/java/org/sonar/server/v2/api/ControllerTester.java index 3b209c36ffe..ff21dc74460 100644 --- a/server/sonar-webserver-webapi-v2/src/testFixtures/java/org/sonar/server/v2/api/ControllerTester.java +++ b/server/sonar-webserver-webapi-v2/src/testFixtures/java/org/sonar/server/v2/api/ControllerTester.java @@ -19,15 +19,29 @@ */ package org.sonar.server.v2.api; +import java.util.List; import org.sonar.server.v2.common.RestResponseEntityExceptionHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; public class ControllerTester { public static MockMvc getMockMvc(Object... controllers) { + return getMockMvcWithHandlerInterceptors(null, controllers); + } + + public static MockMvc getMockMvcWithHandlerInterceptors(List<HandlerInterceptor> handlerInterceptors, Object... controllers) { return MockMvcBuilders .standaloneSetup(controllers) + .setCustomHandlerMapping(() -> resolveRequestMappingHandlerMapping(handlerInterceptors)) .setControllerAdvice(new RestResponseEntityExceptionHandler()) .build(); } + + private static RequestMappingHandlerMapping resolveRequestMappingHandlerMapping(List<HandlerInterceptor> handlerInterceptors) { + RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); + handlerMapping.setInterceptors(handlerInterceptors != null ? handlerInterceptors.toArray() : new Object[0]); + return handlerMapping; + } } |