3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.authentication.event;
22 import com.google.common.base.Joiner;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.stream.Collectors;
27 import org.junit.Before;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.slf4j.event.Level;
31 import org.sonar.api.server.http.HttpRequest;
32 import org.sonar.api.testfixtures.log.LogTester;
33 import org.sonar.api.utils.log.LoggerLevel;
35 import static java.util.Arrays.asList;
36 import static org.assertj.core.api.Assertions.assertThat;
37 import static org.assertj.core.api.Assertions.assertThatThrownBy;
38 import static org.mockito.Mockito.mock;
39 import static org.mockito.Mockito.verifyNoInteractions;
40 import static org.mockito.Mockito.when;
41 import static org.sonar.server.authentication.event.AuthenticationEvent.Method;
42 import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
43 import static org.sonar.server.authentication.event.AuthenticationException.newBuilder;
45 public class AuthenticationEventImplTest {
46 private static final String LOGIN_129_CHARS = "012345678901234567890123456789012345678901234567890123456789" +
47 "012345678901234567890123456789012345678901234567890123456789012345678";
50 public LogTester logTester = new LogTester();
52 private final AuthenticationEventImpl underTest = new AuthenticationEventImpl();
56 logTester.setLevel(LoggerLevel.DEBUG);
60 public void login_success_fails_with_NPE_if_request_is_null() {
61 logTester.setLevel(LoggerLevel.INFO);
63 Source sso = Source.sso();
64 assertThatThrownBy(() -> underTest.loginSuccess(null, "login", sso))
65 .isInstanceOf(NullPointerException.class)
66 .hasMessage("request can't be null");
70 public void login_success_fails_with_NPE_if_source_is_null() {
71 logTester.setLevel(LoggerLevel.INFO);
73 assertThatThrownBy(() -> underTest.loginSuccess(mock(HttpRequest.class), "login", null))
74 .isInstanceOf(NullPointerException.class)
75 .hasMessage("source can't be null");
79 public void login_success_does_not_interact_with_request_if_log_level_is_above_DEBUG() {
80 HttpRequest request = mock(HttpRequest.class);
81 logTester.setLevel(LoggerLevel.INFO);
83 underTest.loginSuccess(request, "login", Source.sso());
85 verifyNoInteractions(request);
89 public void login_success_creates_DEBUG_log_with_empty_login_if_login_argument_is_null() {
90 underTest.loginSuccess(mockRequest(), null, Source.sso());
92 verifyLog("login success [method|SSO][provider|SSO|sso][IP||][login|]");
96 public void login_success_creates_DEBUG_log_with_method_provider_and_login() {
97 underTest.loginSuccess(mockRequest(), "foo", Source.realm(Method.BASIC, "some provider name"));
99 verifyLog("login success [method|BASIC][provider|REALM|some provider name][IP||][login|foo]");
103 public void login_success_prevents_log_flooding_on_login_starting_from_128_chars() {
104 underTest.loginSuccess(mockRequest(), LOGIN_129_CHARS, Source.realm(Method.BASIC, "some provider name"));
106 verifyLog("login success [method|BASIC][provider|REALM|some provider name][IP||][login|012345678901234567890123456789012345678901234567890123456789" +
107 "01234567890123456789012345678901234567890123456789012345678901234567...(129)]");
111 public void login_success_logs_remote_ip_from_request() {
112 underTest.loginSuccess(mockRequest("1.2.3.4"), "foo", Source.realm(Method.EXTERNAL, "bar"));
114 verifyLog("login success [method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|][login|foo]");
118 public void login_success_logs_X_Forwarded_For_header_from_request() {
119 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5"));
120 underTest.loginSuccess(request, "foo", Source.realm(Method.EXTERNAL, "bar"));
122 verifyLog("login success [method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5][login|foo]");
126 public void login_success_logs_X_Forwarded_For_header_from_request_and_supports_multiple_headers() {
127 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5", "6.5.4.3"), asList("9.5.6.7"), asList("6.3.2.4"));
128 underTest.loginSuccess(request, "foo", Source.realm(Method.EXTERNAL, "bar"));
130 verifyLog("login success [method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4][login|foo]");
134 public void login_failure_fails_with_NPE_if_request_is_null() {
135 logTester.setLevel(LoggerLevel.INFO);
137 AuthenticationException exception = newBuilder().setSource(Source.sso()).build();
138 assertThatThrownBy(() -> underTest.loginFailure(null, exception))
139 .isInstanceOf(NullPointerException.class)
140 .hasMessage("request can't be null");
144 public void login_failure_fails_with_NPE_if_AuthenticationException_is_null() {
145 logTester.setLevel(LoggerLevel.INFO);
147 assertThatThrownBy(() -> underTest.loginFailure(mock(HttpRequest.class), null))
148 .isInstanceOf(NullPointerException.class)
149 .hasMessage("AuthenticationException can't be null");
153 public void login_failure_does_not_interact_with_arguments_if_log_level_is_above_DEBUG() {
154 HttpRequest request = mock(HttpRequest.class);
155 AuthenticationException exception = mock(AuthenticationException.class);
156 logTester.setLevel(LoggerLevel.INFO);
158 underTest.loginFailure(request, exception);
160 verifyNoInteractions(request, exception);
164 public void login_failure_creates_DEBUG_log_with_empty_login_if_AuthenticationException_has_no_login() {
165 AuthenticationException exception = newBuilder().setSource(Source.sso()).setMessage("message").build();
166 underTest.loginFailure(mockRequest(), exception);
168 verifyLog("login failure [cause|message][method|SSO][provider|SSO|sso][IP||][login|]");
172 public void login_failure_creates_DEBUG_log_with_empty_cause_if_AuthenticationException_has_no_message() {
173 AuthenticationException exception = newBuilder().setSource(Source.sso()).setLogin("FoO").build();
174 underTest.loginFailure(mockRequest(), exception);
176 verifyLog("login failure [cause|][method|SSO][provider|SSO|sso][IP||][login|FoO]");
180 public void login_failure_creates_DEBUG_log_with_method_provider_and_login() {
181 AuthenticationException exception = newBuilder()
182 .setSource(Source.realm(Method.BASIC, "some provider name"))
183 .setMessage("something got terribly wrong")
186 underTest.loginFailure(mockRequest(), exception);
188 verifyLog("login failure [cause|something got terribly wrong][method|BASIC][provider|REALM|some provider name][IP||][login|BaR]");
192 public void login_failure_prevents_log_flooding_on_login_starting_from_128_chars() {
193 AuthenticationException exception = newBuilder()
194 .setSource(Source.realm(Method.BASIC, "some provider name"))
196 .setLogin(LOGIN_129_CHARS)
198 underTest.loginFailure(mockRequest(), exception);
200 verifyLog("login failure [cause|pop][method|BASIC][provider|REALM|some provider name][IP||][login|012345678901234567890123456789012345678901234567890123456789" +
201 "01234567890123456789012345678901234567890123456789012345678901234567...(129)]");
205 public void login_failure_logs_remote_ip_from_request() {
206 AuthenticationException exception = newBuilder()
207 .setSource(Source.realm(Method.EXTERNAL, "bar"))
208 .setMessage("Damn it!")
211 underTest.loginFailure(mockRequest("1.2.3.4"), exception);
213 verifyLog("login failure [cause|Damn it!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|][login|Baaad]");
217 public void login_failure_logs_X_Forwarded_For_header_from_request() {
218 AuthenticationException exception = newBuilder()
219 .setSource(Source.realm(Method.EXTERNAL, "bar"))
220 .setMessage("Hop la!")
223 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5"));
224 underTest.loginFailure(request, exception);
226 verifyLog("login failure [cause|Hop la!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5][login|foo]");
230 public void login_failure_logs_X_Forwarded_For_header_from_request_and_supports_multiple_headers() {
231 AuthenticationException exception = newBuilder()
232 .setSource(Source.realm(Method.EXTERNAL, "bar"))
236 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5", "6.5.4.3"), asList("9.5.6.7"), asList("6.3.2.4"));
237 underTest.loginFailure(request, exception);
239 verifyLog("login failure [cause|Boom!][method|EXTERNAL][provider|REALM|bar][IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4][login|foo]");
243 public void logout_success_fails_with_NPE_if_request_is_null() {
244 logTester.setLevel(LoggerLevel.INFO);
246 assertThatThrownBy(() -> underTest.logoutSuccess(null, "foo"))
247 .isInstanceOf(NullPointerException.class)
248 .hasMessage("request can't be null");
252 public void logout_success_does_not_interact_with_request_if_log_level_is_above_DEBUG() {
253 HttpRequest request = mock(HttpRequest.class);
254 logTester.setLevel(LoggerLevel.INFO);
256 underTest.logoutSuccess(request, "foo");
258 verifyNoInteractions(request);
262 public void logout_success_creates_DEBUG_log_with_empty_login_if_login_argument_is_null() {
263 underTest.logoutSuccess(mockRequest(), null);
265 verifyLog("logout success [IP||][login|]");
269 public void logout_success_creates_DEBUG_log_with_login() {
270 underTest.logoutSuccess(mockRequest(), "foo");
272 verifyLog("logout success [IP||][login|foo]");
276 public void logout_success_logs_remote_ip_from_request() {
277 underTest.logoutSuccess(mockRequest("1.2.3.4"), "foo");
279 verifyLog("logout success [IP|1.2.3.4|][login|foo]");
283 public void logout_success_logs_X_Forwarded_For_header_from_request() {
284 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5"));
285 underTest.logoutSuccess(request, "foo");
287 verifyLog("logout success [IP|1.2.3.4|2.3.4.5][login|foo]");
291 public void logout_success_logs_X_Forwarded_For_header_from_request_and_supports_multiple_headers() {
292 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5", "6.5.4.3"), asList("9.5.6.7"), asList("6.3.2.4"));
293 underTest.logoutSuccess(request, "foo");
295 verifyLog("logout success [IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4][login|foo]");
299 public void logout_failure_with_NPE_if_request_is_null() {
300 logTester.setLevel(LoggerLevel.INFO);
302 assertThatThrownBy(() -> underTest.logoutFailure(null, "bad csrf"))
303 .isInstanceOf(NullPointerException.class)
304 .hasMessage("request can't be null");
308 public void login_fails_with_NPE_if_error_message_is_null() {
309 logTester.setLevel(LoggerLevel.INFO);
311 assertThatThrownBy(() -> underTest.logoutFailure(mock(HttpRequest.class), null))
312 .isInstanceOf(NullPointerException.class)
313 .hasMessage("error message can't be null");
317 public void logout_does_not_interact_with_request_if_log_level_is_above_DEBUG() {
318 HttpRequest request = mock(HttpRequest.class);
319 logTester.setLevel(LoggerLevel.INFO);
321 underTest.logoutFailure(request, "bad csrf");
323 verifyNoInteractions(request);
327 public void logout_creates_DEBUG_log_with_error() {
328 underTest.logoutFailure(mockRequest(), "bad token");
330 verifyLog("logout failure [error|bad token][IP||]");
334 public void logout_logs_remote_ip_from_request() {
335 underTest.logoutFailure(mockRequest("1.2.3.4"), "bad token");
337 verifyLog("logout failure [error|bad token][IP|1.2.3.4|]");
341 public void logout_logs_X_Forwarded_For_header_from_request() {
342 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5"));
343 underTest.logoutFailure(request, "bad token");
345 verifyLog("logout failure [error|bad token][IP|1.2.3.4|2.3.4.5]");
349 public void logout_logs_X_Forwarded_For_header_from_request_and_supports_multiple_headers() {
350 HttpRequest request = mockRequest("1.2.3.4", asList("2.3.4.5", "6.5.4.3"), asList("9.5.6.7"), asList("6.3.2.4"));
351 underTest.logoutFailure(request, "bad token");
353 verifyLog("logout failure [error|bad token][IP|1.2.3.4|2.3.4.5,6.5.4.3,9.5.6.7,6.3.2.4]");
356 private void verifyLog(String expected) {
357 assertThat(logTester.logs()).hasSize(1);
358 assertThat(logTester.logs(Level.DEBUG))
359 .containsOnly(expected);
362 private static HttpRequest mockRequest() {
363 return mockRequest("");
366 private static HttpRequest mockRequest(String remoteAddr, List<String>... remoteIps) {
367 HttpRequest res = mock(HttpRequest.class);
368 when(res.getRemoteAddr()).thenReturn(remoteAddr);
369 when(res.getHeaders("X-Forwarded-For"))
370 .thenReturn(Collections.enumeration(
371 Arrays.stream(remoteIps)
372 .map(Joiner.on(",")::join)
373 .collect(Collectors.toList())));