From: Jacek Poreda Date: Wed, 6 Dec 2023 13:02:30 +0000 (+0100) Subject: SONAR-21223 Log deprecated API usage for API V1 X-Git-Tag: 10.4.0.87286~310 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7fe24812e4dccac7aebff9b2c5109727a76af56c;p=sonarqube.git SONAR-21223 Log deprecated API usage for API V1 --- diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptor.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptor.java new file mode 100644 index 00000000000..cf87fba6b2c --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptor.java @@ -0,0 +1,90 @@ +/* + * 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.platform.web; + +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.ActionInterceptor; + +/** + * Logs deprecation messages for deprecated web service endpoints and parameters for API V1 + * Messages are logged: + * at DEBUG level for anonymous users and browsers session, + * at WARN level for authenticated users using tokens + */ +public class ActionDeprecationLoggerInterceptor implements ActionInterceptor { + private static final Logger LOGGER = LoggerFactory.getLogger("SONAR_DEPRECATION"); + private final UserSession userSession; + + public ActionDeprecationLoggerInterceptor(UserSession userSession) { + this.userSession = userSession; + } + + @Override + public void preAction(WebService.Action action, Request request) { + Level logLevel = getLogLevel(); + + String deprecatedSinceEndpoint = action.deprecatedSince(); + if (deprecatedSinceEndpoint != null) { + logWebServiceMessage(logLevel, deprecatedSinceEndpoint); + } + + action.params().forEach(param -> logParamMessage(request, logLevel, param)); + } + + private Level getLogLevel() { + return isBrowserSessionOrAnonymous() ? Level.DEBUG : Level.WARN; + } + + private boolean isBrowserSessionOrAnonymous() { + return userSession instanceof ThreadLocalUserSession threadLocalUserSession + && (threadLocalUserSession.hasSession() + && (!userSession.isLoggedIn() || userSession.isAuthenticatedBrowserSession())); + } + + private static void logWebServiceMessage(Level logLevel, String deprecatedSinceEndpoint) { + LOGGER.atLevel(logLevel).log("Web service is deprecated since {} and will be removed in a future version.", deprecatedSinceEndpoint); + } + + private static void logParamMessage(Request request, Level logLevel, WebService.Param param) { + String paramKey = param.key(); + String deprecatedSince = param.deprecatedSince(); + if (request.hasParam(paramKey) && deprecatedSince != null) { + logParamMessage(logLevel, param.key(), deprecatedSince); + } + + String paramDeprecatedKey = param.deprecatedKey(); + String deprecatedKeySince = param.deprecatedKeySince(); + if (paramDeprecatedKey != null && request.hasParam(paramDeprecatedKey) && deprecatedKeySince != null) { + logParamMessage(logLevel, paramDeprecatedKey, deprecatedKeySince); + } + } + + private static void logParamMessage(Level logLevel, String paramKey, @Nullable String deprecatedSince) { + LOGGER.atLevel(logLevel).log("Parameter '{}' is deprecated since {} and will be removed in a future version.", paramKey, deprecatedSince); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptorTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptorTest.java new file mode 100644 index 00000000000..a54f05201c0 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptorTest.java @@ -0,0 +1,184 @@ +/* + * 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.platform.web; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.event.Level; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.testfixtures.log.LogAndArguments; +import org.sonar.api.testfixtures.log.LogTester; +import org.sonar.server.user.ThreadLocalUserSession; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class ActionDeprecationLoggerInterceptorTest { + private final ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class); + + private final ActionDeprecationLoggerInterceptor underTest = new ActionDeprecationLoggerInterceptor(userSession); + + @Rule + public LogTester logTester = new LogTester().setLevel(Level.DEBUG); + + @Test + public void preAction_whenParamAndEndpointAreNotDeprecated_shouldLogNothing() { + WebService.Action action = mock(WebService.Action.class); + when(action.deprecatedSince()).thenReturn(null); + WebService.Param mockParam = mock(WebService.Param.class); + when(mockParam.deprecatedKeySince()).thenReturn(null); + when(action.params()).thenReturn(List.of(mockParam)); + + Request request = mock(Request.class); + + underTest.preAction(action, request); + + verifyNoDeprecatedMsgInLogs(Level.DEBUG); + verifyNoDeprecatedMsgInLogs(Level.WARN); + } + + @Test + @UseDataProvider("userSessions") + public void preAction_whenEndpointIsDeprecatedAndBrowserSession_shouldLogWarning(boolean isLoggedIn, boolean isAuthenticatedBrowserSession, Level expectedLogLevel) { + when(userSession.hasSession()).thenReturn(true); + when(userSession.isLoggedIn()).thenReturn(isLoggedIn); + when(userSession.isAuthenticatedBrowserSession()).thenReturn(isAuthenticatedBrowserSession); + + WebService.Action action = mock(WebService.Action.class); + when(action.path()).thenReturn("api/issues/search"); + when(action.deprecatedSince()).thenReturn("9.8"); + when(action.params()).thenReturn(Collections.emptyList()); + + Request request = mock(Request.class); + + underTest.preAction(action, request); + + assertThat(logTester.logs(expectedLogLevel)) + .contains("Web service is deprecated since 9.8 and will be removed in a future version."); + } + + @Test + @UseDataProvider("userSessions") + public void preAction_whenParameterIsDeprecatedAndHasReplacementAndBrowserSession_shouldLogWarning(boolean isLoggedIn, boolean isAuthenticatedBrowserSession, Level expectedLogLevel) { + when(userSession.hasSession()).thenReturn(true); + when(userSession.isLoggedIn()).thenReturn(isLoggedIn); + when(userSession.isAuthenticatedBrowserSession()).thenReturn(isAuthenticatedBrowserSession); + + WebService.Action action = mock(WebService.Action.class); + when(action.path()).thenReturn("api/issues/search"); + when(action.deprecatedSince()).thenReturn(null); + + WebService.Param mockParam = mock(WebService.Param.class); + when(mockParam.deprecatedKeySince()).thenReturn("9.6"); + when(mockParam.deprecatedKey()).thenReturn("sansTop25"); + when(mockParam.key()).thenReturn("sansTop25New"); + when(action.params()).thenReturn(List.of(mockParam)); + when(action.param("sansTop25")).thenReturn(mockParam); + + Request request = mock(Request.class); + Request.StringParam stringParam = mock(Request.StringParam.class); + when(stringParam.isPresent()).thenReturn(true); + when(request.hasParam("sansTop25")).thenReturn(true); + when(request.getParams()).thenReturn(Map.of("sansTop25", new String[]{})); + + underTest.preAction(action, request); + + assertThat(logTester.logs(expectedLogLevel)) + .contains("Parameter 'sansTop25' is deprecated since 9.6 and will be removed in a future version."); + } + + @Test + @UseDataProvider("userSessions") + public void preAction_whenParameterIsDeprecatedAndNoReplacementAndBrowserSession_shouldLogWarning(boolean isLoggedIn, boolean isAuthenticatedBrowserSession, Level expectedLogLevel) { + when(userSession.hasSession()).thenReturn(true); + when(userSession.isLoggedIn()).thenReturn(isLoggedIn); + when(userSession.isAuthenticatedBrowserSession()).thenReturn(isAuthenticatedBrowserSession); + + WebService.Action action = mock(WebService.Action.class); + when(action.path()).thenReturn("api/issues/search"); + when(action.deprecatedSince()).thenReturn(null); + + WebService.Param mockParam = mock(WebService.Param.class); + when(mockParam.key()).thenReturn("sansTop25"); + when(mockParam.deprecatedSince()).thenReturn("9.7"); + when(action.params()).thenReturn(List.of(mockParam)); + when(action.param("sansTop25")).thenReturn(mockParam); + + Request request = mock(Request.class); + Request.StringParam stringParam = mock(Request.StringParam.class); + when(stringParam.isPresent()).thenReturn(true); + when(request.hasParam("sansTop25")).thenReturn(true); + when(request.getParams()).thenReturn(Map.of("sansTop25", new String[]{})); + + underTest.preAction(action, request); + + assertThat(logTester.logs(expectedLogLevel)) + .contains("Parameter 'sansTop25' is deprecated since 9.7 and will be removed in a future version."); + } + + @Test + public void preAction_whenNewParamWithDeprecatedKeyIsUsed_shouldLogNothing() { + WebService.Action action = mock(WebService.Action.class); + when(action.deprecatedSince()).thenReturn(null); + + WebService.Param mockParam = mock(WebService.Param.class); + when(mockParam.key()).thenReturn("sansTop25New"); + when(mockParam.deprecatedSince()).thenReturn(null); + when(mockParam.deprecatedKeySince()).thenReturn("9.7"); + when(mockParam.deprecatedKey()).thenReturn("sansTop25"); + when(action.params()).thenReturn(List.of(mockParam)); + + Request request = mock(Request.class); + when(request.hasParam("sansTop25New")).thenReturn(true); + when(request.hasParam("sansTop25")).thenReturn(false); + + underTest.preAction(action, request); + + verifyNoDeprecatedMsgInLogs(Level.DEBUG); + verifyNoDeprecatedMsgInLogs(Level.WARN); + } + + @DataProvider + public static Object[][] userSessions() { + return new Object[][] { + {false, false, Level.DEBUG}, + {false, true, Level.DEBUG}, + {true, false, Level.WARN}, + {true, true, Level.DEBUG} + }; + } + + private void verifyNoDeprecatedMsgInLogs(Level level) { + assertThat(logTester.getLogs(level)) + .extracting(LogAndArguments::getRawMsg) + .doesNotContain("Parameter '{}' is deprecated since {} and will be removed in a future version."); + } + +} diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ActionInterceptor.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ActionInterceptor.java new file mode 100644 index 00000000000..2dce4cbe1e1 --- /dev/null +++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ActionInterceptor.java @@ -0,0 +1,34 @@ +/* + * 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.ws; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.WebService; + +/** + * Allows to intercept web service actions + */ +public interface ActionInterceptor { + + /** + * Called before the action is executed + */ + void preAction(WebService.Action action, Request request); +} diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java index e36c6430171..eebe6812486 100644 --- a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java +++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java @@ -27,6 +27,8 @@ import java.util.Locale; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.catalina.connector.ClientAbortException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.api.Startable; import org.sonar.api.impl.ws.ValidatingRequest; import org.sonar.api.server.ServerSide; @@ -34,8 +36,6 @@ import org.sonar.api.server.ws.LocalConnector; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.api.utils.text.JsonWriter; import org.sonar.server.exceptions.BadConfigurationException; import org.sonar.server.exceptions.BadRequestException; @@ -62,11 +62,13 @@ public class WebServiceEngine implements LocalConnector, Startable { private static final Logger LOGGER = LoggerFactory.getLogger(WebServiceEngine.class); private final WebService[] webServices; + private final ActionInterceptor[] actionInterceptors; private WebService.Context context; - public WebServiceEngine(WebService[] webServices) { + public WebServiceEngine(WebService[] webServices, ActionInterceptor[] actionInterceptors) { this.webServices = webServices; + this.actionInterceptors = actionInterceptors; } @Override @@ -100,14 +102,15 @@ public class WebServiceEngine implements LocalConnector, Startable { public void execute(Request request, Response response) { try { ActionExtractor actionExtractor = new ActionExtractor(request.getPath()); - WebService.Action action = getAction(actionExtractor); - checkFound(action, "Unknown url : %s", request.getPath()); + WebService.Action foundAction = getAction(actionExtractor); + WebService.Action action = checkFound(foundAction, "Unknown url : %s", request.getPath()); if (request instanceof ValidatingRequest validatingRequest) { validatingRequest.setAction(action); validatingRequest.setLocalConnector(this); } checkActionExtension(actionExtractor.getExtension()); verifyRequest(action, request); + preAction(action, request); action.handler().handle(request, response); } catch (IllegalArgumentException e) { sendErrors(request, response, e, 400, singletonList(e.getMessage())); @@ -122,6 +125,12 @@ public class WebServiceEngine implements LocalConnector, Startable { } } + private void preAction(WebService.Action action, Request request) { + for (ActionInterceptor interceptor : actionInterceptors) { + interceptor.preAction(action, request); + } + } + @CheckForNull private WebService.Action getAction(ActionExtractor actionExtractor) { String controllerPath = actionExtractor.getController(); diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java index ed9b58f2b6a..e1699ae79cb 100644 --- a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java +++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java @@ -63,12 +63,13 @@ public class WebServiceEngineTest { @Test public void load_ws_definitions_at_startup() { - WebServiceEngine underTest = new WebServiceEngine(new WebService[]{ + WebServiceEngine underTest = new WebServiceEngine(new WebService[] { newWs("api/foo/index", a -> { }), newWs("api/bar/index", a -> { }) - }); + }, + new ActionInterceptor[] {}); underTest.start(); try { assertThat(underTest.controllers()) @@ -81,7 +82,7 @@ public class WebServiceEngineTest { @DataProvider public static Object[][] responseData() { - return new Object[][]{ + return new Object[][] { {"/api/ping", "pong", 200}, {"api/ping", "pong", 200}, {"api/ping.json", "pong", 200}, @@ -147,7 +148,7 @@ public class WebServiceEngineTest { @DataProvider public static String[] verbs() { - return new String[]{ + return new String[] { "PUT", "DELETE", "HEAD", "PATCH", "CONNECT", "OPTIONS", "TRACE" }; } @@ -396,8 +397,9 @@ public class WebServiceEngineTest { public void fail_when_start_in_not_called() { Request request = new TestRequest().setPath("/api/ping"); DumbResponse response = new DumbResponse(); - WebServiceEngine underTest = new WebServiceEngine(new WebService[]{newPingWs(a -> { - })}); + WebServiceEngine underTest = new WebServiceEngine(new WebService[] { + newPingWs(a -> { + })}, new ActionInterceptor[] {}); underTest.execute(request, response); @@ -434,7 +436,7 @@ public class WebServiceEngineTest { } private static Response run(Request request, Response response, WebService... webServices) { - WebServiceEngine underTest = new WebServiceEngine(webServices); + WebServiceEngine underTest = new WebServiceEngine(webServices, new ActionInterceptor[] {}); underTest.start(); try { underTest.execute(request, response); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 21de4a91081..5798a9dadbb 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -169,6 +169,7 @@ import org.sonar.server.platform.SystemInfoWriterModule; import org.sonar.server.platform.WebCoreExtensionsInstaller; import org.sonar.server.platform.db.CheckAnyonePermissionsAtStartup; import org.sonar.server.platform.db.CheckLanguageSpecificParamsAtStartup; +import org.sonar.server.platform.web.ActionDeprecationLoggerInterceptor; import org.sonar.server.platform.web.SonarLintConnectionFilter; import org.sonar.server.platform.web.WebServiceFilter; import org.sonar.server.platform.web.WebServiceReroutingFilter; @@ -375,6 +376,7 @@ public class PlatformLevel4 extends PlatformLevel { new QualityGateWsModule(), // web services + ActionDeprecationLoggerInterceptor.class, WebServiceEngine.class, new WebServicesWsModule(), SonarLintConnectionFilter.class,