]> source.dussan.org Git - sonarqube.git/blob
056092f1be32350dcad7ec106ae938dff6d37de5
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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 com.google.common.collect.ImmutableMap;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.Optional;
27 import javax.annotation.Nullable;
28 import javax.servlet.http.HttpServletRequest;
29 import javax.servlet.http.HttpServletResponse;
30 import org.junit.Before;
31 import org.junit.Rule;
32 import org.junit.Test;
33 import org.sonar.api.config.internal.MapSettings;
34 import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
35 import org.sonar.api.utils.System2;
36 import org.sonar.db.DbTester;
37 import org.sonar.db.audit.AuditPersister;
38 import org.sonar.db.user.GroupDto;
39 import org.sonar.db.user.UserDto;
40 import org.sonar.server.authentication.event.AuthenticationEvent;
41 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
42 import org.sonar.server.authentication.event.AuthenticationException;
43 import org.sonar.server.es.EsTester;
44 import org.sonar.server.user.NewUserNotifier;
45 import org.sonar.server.user.UserUpdater;
46 import org.sonar.server.user.index.UserIndexer;
47 import org.sonar.server.usergroups.DefaultGroupFinder;
48
49 import static java.util.Arrays.stream;
50 import static org.assertj.core.api.Assertions.assertThat;
51 import static org.assertj.core.api.Assertions.assertThatThrownBy;
52 import static org.mockito.ArgumentMatchers.any;
53 import static org.mockito.ArgumentMatchers.anyMap;
54 import static org.mockito.ArgumentMatchers.eq;
55 import static org.mockito.Mockito.mock;
56 import static org.mockito.Mockito.never;
57 import static org.mockito.Mockito.verify;
58 import static org.mockito.Mockito.verifyNoInteractions;
59 import static org.mockito.Mockito.when;
60 import static org.sonar.db.user.UserTesting.newUserDto;
61
62 public class HttpHeadersAuthenticationTest {
63
64   private final MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", "1");
65
66   @Rule
67   public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
68   @Rule
69   public EsTester es = EsTester.create();
70
71   private static final String DEFAULT_LOGIN = "john";
72   private static final String DEFAULT_NAME = "John";
73   private static final String DEFAULT_EMAIL = "john@doo.com";
74   private static final String GROUP1 = "dev";
75   private static final String GROUP2 = "admin";
76   private static final String GROUPS = GROUP1 + "," + GROUP2;
77
78   private static final Long NOW = 1_000_000L;
79   private static final Long CLOSE_REFRESH_TIME = NOW - 1_000L;
80
81   private static final UserDto DEFAULT_USER = newUserDto()
82     .setLogin(DEFAULT_LOGIN)
83     .setName(DEFAULT_NAME)
84     .setEmail(DEFAULT_EMAIL)
85     .setExternalLogin(DEFAULT_LOGIN)
86     .setExternalIdentityProvider("sonarqube");
87
88   private GroupDto group1;
89   private GroupDto group2;
90   private GroupDto sonarUsers;
91
92   private final System2 system2 = mock(System2.class);
93   private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
94
95   private final UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
96
97   private final DefaultGroupFinder defaultGroupFinder = new DefaultGroupFinder(db.getDbClient());
98   private final UserRegistrarImpl userIdentityAuthenticator = new UserRegistrarImpl(db.getDbClient(),
99     new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, defaultGroupFinder, settings.asConfig(), mock(AuditPersister.class), localAuthentication),
100     defaultGroupFinder);
101   private final HttpServletResponse response = mock(HttpServletResponse.class);
102   private final JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
103   private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
104   private final HttpHeadersAuthentication underTest = new HttpHeadersAuthentication(system2, settings.asConfig(), userIdentityAuthenticator, jwtHttpHandler,
105     authenticationEvent);
106
107   @Before
108   public void setUp() {
109     when(system2.now()).thenReturn(NOW);
110     group1 = db.users().insertGroup(GROUP1);
111     group2 = db.users().insertGroup(GROUP2);
112     sonarUsers = db.users().insertDefaultGroup();
113   }
114
115   @Test
116   public void create_user_when_authenticating_new_user() {
117     startWithSso();
118     setNotUserInToken();
119     HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
120
121     underTest.authenticate(request, response);
122
123     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
124     verifyTokenIsUpdated(NOW);
125     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
126   }
127
128   @Test
129   public void use_login_when_name_is_not_provided() {
130     startWithSso();
131     setNotUserInToken();
132
133     HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, null);
134     underTest.authenticate(request, response);
135
136     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, sonarUsers);
137     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
138   }
139
140   @Test
141   public void update_user_when_authenticating_exiting_user() {
142     startWithSso();
143     setNotUserInToken();
144     insertUser(newUserDto().setLogin(DEFAULT_LOGIN).setExternalLogin(DEFAULT_LOGIN).setExternalIdentityProvider("sonarqube").setName("old name").setEmail(DEFAULT_USER.getEmail()), group1);
145     // Name, email and groups are different
146     HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUP2);
147
148     underTest.authenticate(request, response);
149
150     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group2);
151     verifyTokenIsUpdated(NOW);
152     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
153   }
154
155   @Test
156   public void remove_groups_when_group_headers_is_empty() {
157     startWithSso();
158     setNotUserInToken();
159     insertUser(DEFAULT_USER, group1);
160     HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, "");
161
162     underTest.authenticate(request, response);
163
164     verityUserHasNoGroup(DEFAULT_LOGIN);
165     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
166   }
167
168   @Test
169   public void remove_groups_when_group_headers_is_null() {
170     startWithSso();
171     setNotUserInToken();
172     insertUser(DEFAULT_USER, group1);
173     Map<String, String> headerValuesByName = new HashMap<>();
174     headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
175     headerValuesByName.put("X-Forwarded-Email", DEFAULT_USER.getEmail());
176     headerValuesByName.put("X-Forwarded-Groups", null);
177     HttpServletRequest request = createRequest(headerValuesByName);
178
179     underTest.authenticate(request, response);
180
181     verityUserHasNoGroup(DEFAULT_LOGIN);
182     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
183   }
184
185   @Test
186   public void does_not_update_groups_when_no_group_headers() {
187     startWithSso();
188     setNotUserInToken();
189     insertUser(DEFAULT_USER, group1, sonarUsers);
190     HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null);
191
192     underTest.authenticate(request, response);
193
194     verityUserGroups(DEFAULT_LOGIN, group1, sonarUsers);
195     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
196   }
197
198   @Test
199   public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() {
200     startWithSso();
201     UserDto user = insertUser(DEFAULT_USER, group1);
202     setUserInToken(user, CLOSE_REFRESH_TIME);
203     HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
204
205     underTest.authenticate(request, response);
206
207     // User is not updated
208     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
209     verifyTokenIsNotUpdated();
210     verifyNoInteractions(authenticationEvent);
211   }
212
213   @Test
214   public void update_user_when_user_in_token_but_refresh_time_is_old() {
215     startWithSso();
216     UserDto user = insertUser(DEFAULT_USER, group1);
217     // Refresh time was updated 6 minutes ago => more than 5 minutes
218     setUserInToken(user, NOW - 6 * 60 * 1000L);
219     HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), GROUP2);
220
221     underTest.authenticate(request, response);
222
223     // User is updated
224     verifyUserInDb(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), group2);
225     verifyTokenIsUpdated(NOW);
226     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
227   }
228
229   @Test
230   public void update_user_when_user_in_token_but_no_refresh_time() {
231     startWithSso();
232     UserDto user = insertUser(DEFAULT_USER, group1);
233     setUserInToken(user, null);
234     HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), GROUP2);
235
236     underTest.authenticate(request, response);
237
238     // User is updated
239     verifyUserInDb(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), group2);
240     verifyTokenIsUpdated(NOW);
241     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
242   }
243
244   @Test
245   public void use_refresh_time_from_settings() {
246     settings.setProperty("sonar.web.sso.refreshIntervalInMinutes", "10");
247     startWithSso();
248     UserDto user = insertUser(DEFAULT_USER, group1);
249     // Refresh time was updated 6 minutes ago => less than 10 minutes ago so not updated
250     setUserInToken(user, NOW - 6 * 60 * 1000L);
251     HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
252
253     underTest.authenticate(request, response);
254
255     // User is not updated
256     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
257     verifyTokenIsNotUpdated();
258     verifyNoInteractions(authenticationEvent);
259   }
260
261   @Test
262   public void update_user_when_login_from_token_is_different_than_login_from_request() {
263     startWithSso();
264     insertUser(DEFAULT_USER, group1);
265     setUserInToken(DEFAULT_USER, CLOSE_REFRESH_TIME);
266     HttpServletRequest request = createRequest("AnotherLogin", "Another name", "Another email", GROUP2);
267
268     underTest.authenticate(request, response);
269
270     verifyUserInDb("AnotherLogin", "Another name", "Another email", group2, sonarUsers);
271     verifyTokenIsUpdated(NOW);
272     verify(authenticationEvent).loginSuccess(request, "AnotherLogin", Source.sso());
273   }
274
275   @Test
276   public void use_headers_from_settings() {
277     settings.setProperty("sonar.web.sso.loginHeader", "head-login");
278     settings.setProperty("sonar.web.sso.nameHeader", "head-name");
279     settings.setProperty("sonar.web.sso.emailHeader", "head-email");
280     settings.setProperty("sonar.web.sso.groupsHeader", "head-groups");
281     startWithSso();
282     setNotUserInToken();
283     HttpServletRequest request = createRequest(ImmutableMap.of("head-login", DEFAULT_LOGIN, "head-name", DEFAULT_NAME, "head-email", DEFAULT_EMAIL, "head-groups", GROUPS));
284
285     underTest.authenticate(request, response);
286
287     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
288     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
289   }
290
291   @Test
292   public void detect_group_header_even_with_wrong_case() {
293     settings.setProperty("sonar.web.sso.loginHeader", "login");
294     settings.setProperty("sonar.web.sso.nameHeader", "name");
295     settings.setProperty("sonar.web.sso.emailHeader", "email");
296     settings.setProperty("sonar.web.sso.groupsHeader", "Groups");
297     startWithSso();
298     setNotUserInToken();
299     HttpServletRequest request = createRequest(ImmutableMap.of("login", DEFAULT_LOGIN, "name", DEFAULT_NAME, "email", DEFAULT_EMAIL, "groups", GROUPS));
300
301     underTest.authenticate(request, response);
302
303     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
304     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
305   }
306
307   @Test
308   public void trim_groups() {
309     startWithSso();
310     setNotUserInToken();
311     HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, "  dev ,    admin ");
312
313     underTest.authenticate(request, response);
314
315     verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2, sonarUsers);
316     verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
317   }
318
319   @Test
320   public void does_not_authenticate_when_no_header() {
321     startWithSso();
322     setNotUserInToken();
323
324     underTest.authenticate(createRequest(Collections.emptyMap()), response);
325
326     verifyUserNotAuthenticated();
327     verifyTokenIsNotUpdated();
328     verifyNoInteractions(authenticationEvent);
329   }
330
331   @Test
332   public void does_not_authenticate_when_not_enabled() {
333     startWithoutSso();
334
335     underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
336
337     verifyUserNotAuthenticated();
338     verifyNoInteractions(jwtHttpHandler, authenticationEvent);
339   }
340
341   @Test
342   public void throw_AuthenticationException_when_BadRequestException_is_generated() {
343     startWithSso();
344     setNotUserInToken();
345
346     assertThatThrownBy(() -> underTest.authenticate(createRequest("invalid login", DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response))
347       .hasMessage("Login should contain only letters, numbers, and .-_@")
348       .isInstanceOf(AuthenticationException.class)
349       .hasFieldOrPropertyWithValue("source", Source.sso());
350
351     verifyNoInteractions(authenticationEvent);
352   }
353
354   private void startWithSso() {
355     settings.setProperty("sonar.web.sso.enable", true);
356     underTest.start();
357   }
358
359   private void startWithoutSso() {
360     settings.setProperty("sonar.web.sso.enable", false);
361     underTest.start();
362   }
363
364   private void setUserInToken(UserDto user, @Nullable Long lastRefreshTime) {
365     when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
366       .thenReturn(Optional.of(new JwtHttpHandler.Token(
367         user,
368         lastRefreshTime == null ? Collections.emptyMap() : ImmutableMap.of("ssoLastRefreshTime", lastRefreshTime))));
369   }
370
371   private void setNotUserInToken() {
372     when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(Optional.empty());
373   }
374
375   private UserDto insertUser(UserDto user, GroupDto... groups) {
376     db.users().insertUser(user);
377     stream(groups).forEach(group -> db.users().insertMember(group, user));
378     db.commit();
379     return user;
380   }
381
382   private static HttpServletRequest createRequest(Map<String, String> headerValuesByName) {
383     HttpServletRequest request = mock(HttpServletRequest.class);
384     setHeaders(request, headerValuesByName);
385     return request;
386   }
387
388   private static HttpServletRequest createRequest(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
389     Map<String, String> headerValuesByName = new HashMap<>();
390     headerValuesByName.put("X-Forwarded-Login", login);
391     if (name != null) {
392       headerValuesByName.put("X-Forwarded-Name", name);
393     }
394     if (email != null) {
395       headerValuesByName.put("X-Forwarded-Email", email);
396     }
397     if (groups != null) {
398       headerValuesByName.put("X-Forwarded-Groups", groups);
399     }
400     HttpServletRequest request = mock(HttpServletRequest.class);
401     setHeaders(request, headerValuesByName);
402     return request;
403   }
404
405   private static void setHeaders(HttpServletRequest request, Map<String, String> valuesByName) {
406     valuesByName.forEach((key, value) -> when(request.getHeader(key)).thenReturn(value));
407     when(request.getHeaderNames()).thenReturn(Collections.enumeration(valuesByName.keySet()));
408   }
409
410   private void verifyUserInDb(String expectedLogin, String expectedName, @Nullable String expectedEmail, GroupDto... expectedGroups) {
411     UserDto userDto = db.users().selectUserByLogin(expectedLogin).get();
412     assertThat(userDto.isActive()).isTrue();
413     assertThat(userDto.getName()).isEqualTo(expectedName);
414     assertThat(userDto.getEmail()).isEqualTo(expectedEmail);
415     assertThat(userDto.getExternalLogin()).isEqualTo(expectedLogin);
416     assertThat(userDto.getExternalIdentityProvider()).isEqualTo("sonarqube");
417     verityUserGroups(expectedLogin, expectedGroups);
418   }
419
420   private void verityUserGroups(String login, GroupDto... expectedGroups) {
421     UserDto userDto = db.users().selectUserByLogin(login).get();
422     if (expectedGroups.length == 0) {
423       assertThat(db.users().selectGroupUuidsOfUser(userDto)).isEmpty();
424     } else {
425       assertThat(db.users().selectGroupUuidsOfUser(userDto)).containsOnly(stream(expectedGroups).map(GroupDto::getUuid).toArray(String[]::new));
426     }
427   }
428
429   private void verityUserHasNoGroup(String login) {
430     verityUserGroups(login);
431   }
432
433   private void verifyUserNotAuthenticated() {
434     assertThat(db.countRowsOfTable(db.getSession(), "users")).isZero();
435     verifyTokenIsNotUpdated();
436   }
437
438   private void verifyTokenIsUpdated(long refreshTime) {
439     verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(ImmutableMap.of("ssoLastRefreshTime", refreshTime)), any(HttpServletRequest.class), any(HttpServletResponse.class));
440   }
441
442   private void verifyTokenIsNotUpdated() {
443     verify(jwtHttpHandler, never()).generateToken(any(UserDto.class), anyMap(), any(HttpServletRequest.class), any(HttpServletResponse.class));
444   }
445 }