]> source.dussan.org Git - sonarqube.git/blob
b964228838bfda3f9c142a44935a6854fdc05041
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.server.authentication;
21
22 import java.time.LocalDateTime;
23 import java.time.ZoneOffset;
24 import org.junit.Before;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.mockito.ArgumentCaptor;
28 import org.slf4j.MDC;
29 import org.sonar.api.config.internal.MapSettings;
30 import org.sonar.api.server.authentication.BaseIdentityProvider;
31 import org.sonar.api.server.http.Cookie;
32 import org.sonar.api.server.http.HttpRequest;
33 import org.sonar.api.server.http.HttpResponse;
34 import org.sonar.api.utils.System2;
35 import org.sonar.db.DbTester;
36 import org.sonar.db.user.UserDto;
37 import org.sonar.db.user.UserTokenDto;
38 import org.sonar.server.authentication.event.AuthenticationEvent;
39 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
40 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
41 import org.sonar.server.authentication.event.AuthenticationException;
42 import org.sonar.server.tester.AnonymousMockUserSession;
43 import org.sonar.server.tester.MockUserSession;
44 import org.sonar.server.user.ThreadLocalUserSession;
45 import org.sonar.server.user.TokenUserSession;
46 import org.sonar.server.user.UserSession;
47
48 import static org.assertj.core.api.Assertions.assertThat;
49 import static org.mockito.ArgumentMatchers.eq;
50 import static org.mockito.Mockito.doThrow;
51 import static org.mockito.Mockito.mock;
52 import static org.mockito.Mockito.reset;
53 import static org.mockito.Mockito.verify;
54 import static org.mockito.Mockito.verifyNoMoreInteractions;
55 import static org.mockito.Mockito.when;
56 import static org.sonar.api.utils.DateUtils.formatDateTime;
57
58 public class UserSessionInitializerIT {
59
60   @Rule
61   public DbTester dbTester = DbTester.create(System2.INSTANCE);
62
63   private ThreadLocalUserSession threadLocalSession = mock(ThreadLocalUserSession.class);
64   private HttpRequest request = mock(HttpRequest.class);
65   private HttpResponse response = mock(HttpResponse.class);
66   private RequestAuthenticator authenticator = mock(RequestAuthenticator.class);
67   private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
68   private MapSettings settings = new MapSettings();
69   private ArgumentCaptor<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
70
71   private UserSessionInitializer underTest = new UserSessionInitializer(settings.asConfig(), threadLocalSession, authenticationEvent, authenticator);
72
73   @Before
74   public void setUp() {
75     when(request.getContextPath()).thenReturn("");
76     when(request.getRequestURI()).thenReturn("/measures");
77   }
78
79   @Test
80   public void check_urls() {
81     assertPathIsNotIgnored("/");
82     assertPathIsNotIgnored("/foo");
83     assertPathIsNotIgnored("/api/server_id/show");
84
85     assertPathIsIgnored("/api/authentication/login");
86     assertPathIsIgnored("/api/authentication/logout");
87     assertPathIsIgnored("/api/authentication/validate");
88     assertPathIsIgnored("/batch/index");
89     assertPathIsIgnored("/batch/file");
90     assertPathIsIgnored("/maintenance/index");
91     assertPathIsIgnored("/setup/index");
92     assertPathIsIgnored("/sessions/new");
93     assertPathIsIgnored("/sessions/logout");
94     assertPathIsIgnored("/sessions/unauthorized");
95     assertPathIsIgnored("/oauth2/callback/github");
96     assertPathIsIgnored("/oauth2/callback/foo");
97     assertPathIsIgnored("/api/system/db_migration_status");
98     assertPathIsIgnored("/api/system/status");
99     assertPathIsIgnored("/api/system/migrate_db");
100     assertPathIsIgnored("/api/server/version");
101     assertPathIsIgnored("/api/users/identity_providers");
102     assertPathIsIgnored("/api/l10n/index");
103
104     // exclude project_badge url, as they can be auth. by a token as queryparam
105     assertPathIsIgnored("/api/project_badges/measure");
106     assertPathIsIgnored("/api/project_badges/quality_gate");
107
108     // exlude passcode urls
109     assertPathIsIgnoredWithAnonymousAccess("/api/ce/info");
110     assertPathIsIgnoredWithAnonymousAccess("/api/ce/pause");
111     assertPathIsIgnoredWithAnonymousAccess("/api/ce/resume");
112     assertPathIsIgnoredWithAnonymousAccess("/api/system/health");
113     assertPathIsIgnoredWithAnonymousAccess("/api/system/liveness");
114     assertPathIsIgnoredWithAnonymousAccess("/api/monitoring/metrics");
115
116     // exclude static resources
117     assertPathIsIgnored("/css/style.css");
118     assertPathIsIgnored("/images/logo.png");
119     assertPathIsIgnored("/js/jquery.js");
120   }
121
122   @Test
123   public void return_code_401_when_not_authenticated_and_with_force_authentication() {
124     ArgumentCaptor<AuthenticationException> exceptionArgumentCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
125     when(threadLocalSession.isLoggedIn()).thenReturn(false);
126     when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
127
128     assertThat(underTest.initUserSession(request, response)).isTrue();
129
130     verifyNoMoreInteractions(response);
131     verify(authenticationEvent).loginFailure(eq(request), exceptionArgumentCaptor.capture());
132     verifyNoMoreInteractions(threadLocalSession);
133     AuthenticationException authenticationException = exceptionArgumentCaptor.getValue();
134     assertThat(authenticationException.getSource()).isEqualTo(Source.local(Method.BASIC));
135     assertThat(authenticationException.getLogin()).isNull();
136     assertThat(authenticationException.getMessage()).isEqualTo("User must be authenticated");
137     assertThat(authenticationException.getPublicMessage()).isNull();
138   }
139
140   @Test
141   public void does_not_return_code_401_when_not_authenticated_and_with_force_authentication_off() {
142     when(threadLocalSession.isLoggedIn()).thenReturn(false);
143     when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
144     settings.setProperty("sonar.forceAuthentication", false);
145
146     assertThat(underTest.initUserSession(request, response)).isTrue();
147
148     verifyNoMoreInteractions(response);
149   }
150
151   @Test
152   public void return_401_and_stop_on_ws() {
153     when(request.getRequestURI()).thenReturn("/api/issues");
154     AuthenticationException authenticationException = AuthenticationException.newBuilder().setSource(Source.jwt()).setMessage("Token id hasn't been found").build();
155     doThrow(authenticationException).when(authenticator).authenticate(request, response);
156
157     assertThat(underTest.initUserSession(request, response)).isFalse();
158
159     verify(response).setStatus(401);
160     verify(authenticationEvent).loginFailure(request, authenticationException);
161     verifyNoMoreInteractions(threadLocalSession);
162   }
163
164   @Test
165   public void return_401_and_stop_on_batch_ws() {
166     when(request.getRequestURI()).thenReturn("/batch/global");
167     doThrow(AuthenticationException.newBuilder().setSource(Source.jwt()).setMessage("Token id hasn't been found").build())
168       .when(authenticator).authenticate(request, response);
169
170     assertThat(underTest.initUserSession(request, response)).isFalse();
171
172     verify(response).setStatus(401);
173     verifyNoMoreInteractions(threadLocalSession);
174   }
175
176   @Test
177   public void return_to_session_unauthorized_when_error_on_from_external_provider() throws Exception {
178     doThrow(AuthenticationException.newBuilder().setSource(Source.external(newBasicIdentityProvider("failing"))).setPublicMessage("Token id hasn't been found").build())
179       .when(authenticator).authenticate(request, response);
180
181     assertThat(underTest.initUserSession(request, response)).isFalse();
182
183     verify(response).sendRedirect("/sessions/unauthorized");
184     verify(response).addCookie(cookieArgumentCaptor.capture());
185     Cookie cookie = cookieArgumentCaptor.getValue();
186     assertThat(cookie.getName()).isEqualTo("AUTHENTICATION-ERROR");
187     assertThat(cookie.getValue()).isEqualTo("Token%20id%20hasn%27t%20been%20found");
188     assertThat(cookie.getPath()).isEqualTo("/");
189     assertThat(cookie.isHttpOnly()).isFalse();
190     assertThat(cookie.getMaxAge()).isEqualTo(300);
191     assertThat(cookie.isSecure()).isFalse();
192   }
193
194   @Test
195   public void return_to_session_unauthorized_when_error_on_from_external_provider_with_context_path() throws Exception {
196     when(request.getContextPath()).thenReturn("/sonarqube");
197     doThrow(AuthenticationException.newBuilder().setSource(Source.external(newBasicIdentityProvider("failing"))).setPublicMessage("Token id hasn't been found").build())
198       .when(authenticator).authenticate(request, response);
199
200     assertThat(underTest.initUserSession(request, response)).isFalse();
201
202     verify(response).sendRedirect("/sonarqube/sessions/unauthorized");
203   }
204
205   @Test
206   public void expiration_header_added_when_authenticating_with_an_expiring_token() {
207     long expirationTimestamp = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
208     UserDto userDto = new UserDto();
209     UserTokenDto userTokenDto = new UserTokenDto().setExpirationDate(expirationTimestamp);
210     UserSession session = new TokenUserSession(DbTester.create().getDbClient(), userDto, userTokenDto);
211
212     when(authenticator.authenticate(request, response)).thenReturn(session);
213     when(threadLocalSession.isLoggedIn()).thenReturn(true);
214
215     assertThat(underTest.initUserSession(request, response)).isTrue();
216     verify(response).addHeader("SonarQube-Authentication-Token-Expiration", formatDateTime(expirationTimestamp));
217   }
218
219   @Test
220   public void initUserSession_shouldPutLoginInMDC() {
221     when(threadLocalSession.isLoggedIn()).thenReturn(false);
222     when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user"));
223
224     underTest.initUserSession(request, response);
225
226     assertThat(MDC.get("LOGIN")).isEqualTo("user");
227   }
228
229   @Test
230   public void initUserSession_whenSessionLoginIsNull_shouldPutDefaultLoginValueInMDC() {
231     when(threadLocalSession.isLoggedIn()).thenReturn(false);
232     when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
233
234     underTest.initUserSession(request, response);
235
236     assertThat(MDC.get("LOGIN")).isEqualTo("-");
237   }
238
239   @Test
240   public void removeUserSession_shoudlRemoveMDCLogin() {
241     when(threadLocalSession.isLoggedIn()).thenReturn(false);
242     when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user"));
243     underTest.initUserSession(request, response);
244
245     underTest.removeUserSession();
246
247     assertThat(MDC.get("LOGIN")).isNull();
248   }
249
250   private void assertPathIsIgnored(String path) {
251     when(request.getRequestURI()).thenReturn(path);
252
253     assertThat(underTest.initUserSession(request, response)).isTrue();
254
255     verifyNoMoreInteractions(threadLocalSession, authenticator);
256     reset(threadLocalSession, authenticator);
257   }
258
259   private void assertPathIsIgnoredWithAnonymousAccess(String path) {
260     when(request.getRequestURI()).thenReturn(path);
261     UserSession session = new AnonymousMockUserSession();
262     when(authenticator.authenticate(request, response)).thenReturn(session);
263
264     assertThat(underTest.initUserSession(request, response)).isTrue();
265
266     verify(threadLocalSession).set(session);
267     reset(threadLocalSession, authenticator);
268   }
269
270   private void assertPathIsNotIgnored(String path) {
271     when(request.getRequestURI()).thenReturn(path);
272     UserSession session = new MockUserSession("foo");
273     when(authenticator.authenticate(request, response)).thenReturn(session);
274
275     assertThat(underTest.initUserSession(request, response)).isTrue();
276
277     verify(threadLocalSession).set(session);
278     reset(threadLocalSession, authenticator);
279   }
280
281   private static BaseIdentityProvider newBasicIdentityProvider(String name) {
282     BaseIdentityProvider mock = mock(BaseIdentityProvider.class);
283     when(mock.getName()).thenReturn(name);
284     return mock;
285   }
286 }