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