From: Simon Brandhof Date: Sat, 6 Aug 2016 08:17:05 +0000 (+0200) Subject: SONAR-7581 ability to have user login in access.log X-Git-Tag: 6.1-RC1~438 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e55369090f3be4f1e4baa791b05c3f5cf1cef6e1;p=sonarqube.git SONAR-7581 ability to have user login in access.log --- diff --git a/it/it-tests/src/test/java/it/Category4Suite.java b/it/it-tests/src/test/java/it/Category4Suite.java index 15474220cbd..915a1ed8395 100644 --- a/it/it-tests/src/test/java/it/Category4Suite.java +++ b/it/it-tests/src/test/java/it/Category4Suite.java @@ -34,6 +34,7 @@ import it.http.HttpHeadersTest; import it.projectComparison.ProjectComparisonTest; import it.projectEvent.EventTest; import it.qualityProfile.QualityProfilesPageTest; +import it.serverSystem.LogsTest; import it.serverSystem.ServerSystemTest; import it.ui.UiTest; import it.uiExtension.UiExtensionsTest; @@ -95,7 +96,8 @@ import static util.ItUtils.xooPlugin; WsLocalCallTest.class, WsTest.class, // quality profiles - QualityProfilesPageTest.class + QualityProfilesPageTest.class, + LogsTest.class }) public class Category4Suite { @@ -117,5 +119,8 @@ public class Category4Suite { // Used by WsLocalCallTest .addPlugin(pluginArtifact("ws-plugin")) + + // Used by LogsTest + .setServerProperty("sonar.web.accessLogs.pattern", LogsTest.ACCESS_LOGS_PATTERN) .build(); } diff --git a/it/it-tests/src/test/java/it/serverSystem/LogsTest.java b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java new file mode 100644 index 00000000000..bd77db688bc --- /dev/null +++ b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 it.serverSystem; + +import com.sonar.orchestrator.Orchestrator; +import it.Category4Suite; +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.junit.ClassRule; +import org.junit.Test; +import org.sonarqube.ws.client.GetRequest; +import org.sonarqube.ws.client.WsClient; +import util.ItUtils; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; + +public class LogsTest { + + public static final String ACCESS_LOGS_PATTERN = "\"%reqAttribute{LOGIN}\" \"%r\" %s"; + private static final String PATH = "/called/from/LogsTest"; + + @ClassRule + public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR; + + /** + * SONAR-7581 + */ + @Test + public void test_access_logs() throws Exception { + // log "-" for anonymous + sendHttpRequest(ItUtils.newWsClient(orchestrator), PATH); + assertThat(accessLogsFile()).isFile().exists(); + verifyLastAccessLogLine("-", PATH, 404); + + sendHttpRequest(ItUtils.newAdminWsClient(orchestrator), PATH); + verifyLastAccessLogLine("admin", PATH, 404); + } + + private void verifyLastAccessLogLine(String login, String path, int status) throws IOException { + assertThat(readLastAccessLog()).isEqualTo(format("\"%s\" \"GET %s HTTP/1.1\" %d", login, path, status)); + } + + private String readLastAccessLog() throws IOException { + try (ReversedLinesFileReader tailer = new ReversedLinesFileReader(accessLogsFile())) { + return tailer.readLine(); + } + } + + private void sendHttpRequest(WsClient client, String path) { + client.wsConnector().call(new GetRequest(path)); + } + + private File accessLogsFile() { + return new File(orchestrator.getServer().getHome(), "logs/access.log"); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java index 3b2053eee66..f29a0807c74 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -20,15 +20,6 @@ package org.sonar.server.authentication; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; -import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; -import static org.sonar.api.web.ServletFilter.UrlPattern; -import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; -import static org.sonar.server.authentication.ws.LoginAction.AUTH_LOGIN_URL; -import static org.sonar.server.authentication.ws.ValidateAction.AUTH_VALIDATE_URL; -import static org.sonar.server.user.ServerUserSession.createForAnonymous; -import static org.sonar.server.user.ServerUserSession.createForUser; - import com.google.common.collect.ImmutableSet; import java.util.Optional; import java.util.Set; @@ -39,11 +30,28 @@ import org.sonar.api.server.ServerSide; import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ServerUserSession; import org.sonar.server.user.ThreadLocalUserSession; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; +import static org.sonar.api.web.ServletFilter.UrlPattern; +import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; +import static org.sonar.server.authentication.ws.LoginAction.AUTH_LOGIN_URL; +import static org.sonar.server.authentication.ws.ValidateAction.AUTH_VALIDATE_URL; +import static org.sonar.server.user.ServerUserSession.createForAnonymous; +import static org.sonar.server.user.ServerUserSession.createForUser; + @ServerSide public class UserSessionInitializer { + /** + * Key of attribute to be used for displaying user login + * in logs/access.log. The pattern to be configured + * in property sonar.web.accessLogs.pattern is "%reqAttribute{LOGIN}" + */ + public static final String ACCESS_LOG_LOGIN = "LOGIN"; + // SONAR-6546 these urls should be get from WebService private static final Set SKIPPED_URLS = ImmutableSet.of( "/batch/index", "/batch/file", @@ -64,15 +72,15 @@ public class UserSessionInitializer { private final Settings settings; private final JwtHttpHandler jwtHttpHandler; private final BasicAuthenticator basicAuthenticator; - private final ThreadLocalUserSession userSession; + private final ThreadLocalUserSession threadLocalSession; public UserSessionInitializer(DbClient dbClient, Settings settings, JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator, - ThreadLocalUserSession userSession) { + ThreadLocalUserSession threadLocalSession) { this.dbClient = dbClient; this.settings = settings; this.jwtHttpHandler = jwtHttpHandler; this.basicAuthenticator = basicAuthenticator; - this.userSession = userSession; + this.threadLocalSession = threadLocalSession; } public boolean initUserSession(HttpServletRequest request, HttpServletResponse response) { @@ -97,17 +105,20 @@ public class UserSessionInitializer { private void setUserSession(HttpServletRequest request, HttpServletResponse response) { Optional user = authenticate(request, response); if (user.isPresent()) { - userSession.set(createForUser(dbClient, user.get())); + ServerUserSession session = createForUser(dbClient, user.get()); + threadLocalSession.set(session); + request.setAttribute(ACCESS_LOG_LOGIN, session.getLogin()); } else { if (settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY)) { throw new UnauthorizedException("User must be authenticated"); } - userSession.set(createForAnonymous(dbClient)); + threadLocalSession.set(createForAnonymous(dbClient)); + request.setAttribute(ACCESS_LOG_LOGIN, "-"); } } public void removeUserSession() { - userSession.remove(); + threadLocalSession.remove(); } // Try first to authenticate from JWT token, then try from basic http header diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java index 1e75fe2279a..55e0b0cda3d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java @@ -19,12 +19,6 @@ */ package org.sonar.server.user; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -37,17 +31,22 @@ import org.sonar.core.platform.ComponentContainer; import org.sonar.server.authentication.UserSessionInitializer; import org.sonar.server.platform.Platform; -public class UserSessionFilterTest { +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; - UserSessionInitializer userSessionInitializer = mock(UserSessionInitializer.class); - Platform platform = mock(Platform.class); - ComponentContainer componentContainer = mock(ComponentContainer.class); +public class UserSessionFilterTest { - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - FilterChain chain = mock(FilterChain.class); + private UserSessionInitializer userSessionInitializer = mock(UserSessionInitializer.class); + private Platform platform = mock(Platform.class); + private ComponentContainer componentContainer = mock(ComponentContainer.class); + private HttpServletRequest request = mock(HttpServletRequest.class); + private HttpServletResponse response = mock(HttpServletResponse.class); + private FilterChain chain = mock(FilterChain.class); - UserSessionFilter underTest = new UserSessionFilter(platform); + private UserSessionFilter underTest = new UserSessionFilter(platform); @Before public void setUp() { diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index f2912583808..64641e13148 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -244,6 +244,8 @@ # - "common" is the Common Log Format, shortcut to: %h %l %u %user %date "%r" %s %b # - "combined" is another format widely recognized, shortcut to: %h %l %u [%t] "%r" %s %b "%i{Referer}" "%i{User-Agent}" # - else a custom pattern. See http://logback.qos.ch/manual/layouts.html#AccessPatternLayout. +# The login of authenticated user is not implemented with "%u" but with "%reqAttribute{LOGIN}" (since version 6.1). +# The value displayed for anonymous users is "-". # If SonarQube is behind a reverse proxy, then the following value allows to display the correct remote IP address: #sonar.web.accessLogs.pattern=%i{X-Forwarded-For} %l %u [%t] "%r" %s %b "%i{Referer}" "%i{User-Agent}" # Default value is: