]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21223 Log deprecated API usage for API V1
authorJacek Poreda <jacek.poreda@sonarsource.com>
Wed, 6 Dec 2023 13:02:30 +0000 (14:02 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Dec 2023 20:02:55 +0000 (20:02 +0000)
server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptor.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/ActionDeprecationLoggerInterceptorTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ActionInterceptor.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

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 (file)
index 0000000..cf87fba
--- /dev/null
@@ -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 (file)
index 0000000..a54f052
--- /dev/null
@@ -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 (file)
index 0000000..2dce4cb
--- /dev/null
@@ -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);
+}
index e36c643017178d61deaad6d24d594f998847a1dc..eebe68124869ea94eaf7aa5fafb16879bb974b6e 100644 (file)
@@ -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();
index ed9b58f2b6a1fe89cab51e0300fe934379e1371f..e1699ae79cbd3f040bf4c2174d47815e536b4688 100644 (file)
@@ -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);
index 21de4a91081b810dd5aa8026be347ff01a02364e..5798a9dadbbca9eac8617c3e3b29424dc9227767 100644 (file)
@@ -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,