--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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.");
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+}
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;
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;
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
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()));
}
}
+ 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();
@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())
@DataProvider
public static Object[][] responseData() {
- return new Object[][]{
+ return new Object[][] {
{"/api/ping", "pong", 200},
{"api/ping", "pong", 200},
{"api/ping.json", "pong", 200},
@DataProvider
public static String[] verbs() {
- return new String[]{
+ return new String[] {
"PUT", "DELETE", "HEAD", "PATCH", "CONNECT", "OPTIONS", "TRACE"
};
}
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);
}
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);
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;
new QualityGateWsModule(),
// web services
+ ActionDeprecationLoggerInterceptor.class,
WebServiceEngine.class,
new WebServicesWsModule(),
SonarLintConnectionFilter.class,