ソースを参照

SONAR-21223 Log deprecated API usage for API V1

tags/10.4.0.87286
Jacek Poreda 5ヶ月前
コミット
7fe24812e4

+ 90
- 0
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);
}

}

+ 184
- 0
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.");
}

}

+ 34
- 0
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);
}

+ 14
- 5
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();

+ 9
- 7
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);

+ 2
- 0
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,

読み込み中…
キャンセル
保存