@@ -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); | |||
} | |||
} |
@@ -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."); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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(); |
@@ -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); |
@@ -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, |