You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

IntegrationTest.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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.auth.gitlab;
  21. import javax.servlet.http.HttpServletRequest;
  22. import okhttp3.mockwebserver.MockResponse;
  23. import okhttp3.mockwebserver.MockWebServer;
  24. import org.junit.Before;
  25. import org.junit.Rule;
  26. import org.junit.Test;
  27. import org.mockito.ArgumentCaptor;
  28. import org.mockito.Mockito;
  29. import org.sonar.api.config.internal.MapSettings;
  30. import org.sonar.api.server.authentication.OAuth2IdentityProvider;
  31. import org.sonar.api.server.authentication.UnauthorizedException;
  32. import org.sonar.api.server.authentication.UserIdentity;
  33. import static java.lang.String.format;
  34. import static org.assertj.core.api.Assertions.assertThat;
  35. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  36. import static org.mockito.ArgumentMatchers.any;
  37. import static org.mockito.Mockito.verify;
  38. import static org.mockito.Mockito.when;
  39. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOWED_GROUPS;
  40. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP;
  41. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID;
  42. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED;
  43. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET;
  44. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS;
  45. import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
  46. public class IntegrationTest {
  47. private static final String ANY_CODE_VALUE = "ANY_CODE";
  48. @Rule
  49. public MockWebServer gitlab = new MockWebServer();
  50. private final MapSettings mapSettings = new MapSettings();
  51. private final GitLabSettings gitLabSettings = new GitLabSettings(mapSettings.asConfig());
  52. private String gitLabUrl;
  53. private final GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings,
  54. new GitLabRestClient(gitLabSettings),
  55. new ScribeGitLabOauth2Api(gitLabSettings));
  56. @Before
  57. public void setUp() {
  58. this.gitLabUrl = format("http://%s:%d", gitlab.getHostName(), gitlab.getPort());
  59. mapSettings
  60. .setProperty(GITLAB_AUTH_ENABLED, "true")
  61. .setProperty(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, "true")
  62. .setProperty(GITLAB_AUTH_URL, gitLabUrl)
  63. .setProperty(GITLAB_AUTH_APPLICATION_ID, "123")
  64. .setProperty(GITLAB_AUTH_SECRET, "456")
  65. .setProperty(GITLAB_AUTH_ALLOWED_GROUPS, "group1,group2")
  66. .setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "true");
  67. }
  68. @Test
  69. public void callback_whenAllowedUser_shouldAuthenticate() {
  70. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  71. mockAccessTokenResponse();
  72. mockUserResponse();
  73. mockSingleGroupReponse("group1");
  74. gitLabIdentityProvider.callback(callbackContext);
  75. ArgumentCaptor<UserIdentity> argument = ArgumentCaptor.forClass(UserIdentity.class);
  76. verify(callbackContext).authenticate(argument.capture());
  77. assertThat(argument.getValue()).isNotNull();
  78. assertThat(argument.getValue().getProviderId()).isEqualTo("123");
  79. assertThat(argument.getValue().getProviderLogin()).isEqualTo("toto");
  80. assertThat(argument.getValue().getName()).isEqualTo("Toto Toto");
  81. assertThat(argument.getValue().getEmail()).isEqualTo("toto@toto.com");
  82. verify(callbackContext).redirectToRequestedPage();
  83. }
  84. @Test
  85. public void callback_whenGroupNotAllowedAndGroupSyncEnabled_shouldThrow() {
  86. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  87. mockAccessTokenResponse();
  88. mockUserResponse();
  89. mockSingleGroupReponse("wrong-group");
  90. assertThatThrownBy(() -> gitLabIdentityProvider.callback(callbackContext))
  91. .isInstanceOf((UnauthorizedException.class))
  92. .hasMessage("You are not allowed to authenticate");
  93. }
  94. @Test
  95. public void callback_whenGroupNotAllowedAndGroupSyncDisabled_shouldThrow() {
  96. mapSettings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "false");
  97. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  98. mockAccessTokenResponse();
  99. mockUserResponse();
  100. mockSingleGroupReponse("wrong-group");
  101. gitLabIdentityProvider.callback(callbackContext);
  102. verify(callbackContext).authenticate(any());
  103. verify(callbackContext).redirectToRequestedPage();
  104. }
  105. @Test
  106. public void callback_whenAllowedUserBySubgroupMembership_shouldAuthenticate() {
  107. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  108. mockAccessTokenResponse();
  109. mockUserResponse();
  110. mockSingleGroupReponse("group1/subgroup");
  111. gitLabIdentityProvider.callback(callbackContext);
  112. verify(callbackContext).authenticate(any());
  113. verify(callbackContext).redirectToRequestedPage();
  114. }
  115. @Test
  116. public void callback_shouldSynchronizeGroups() throws InterruptedException {
  117. mapSettings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "true");
  118. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  119. mockAccessTokenResponse();
  120. mockUserResponse();
  121. // Response for /groups
  122. gitlab.enqueue(new MockResponse().setBody("""
  123. [
  124. {
  125. "id": 1,
  126. "full_path": "group1"
  127. },
  128. {
  129. "id": 2,
  130. "full_path": "group2"
  131. }
  132. ]
  133. """));
  134. gitLabIdentityProvider.callback(callbackContext);
  135. ArgumentCaptor<UserIdentity> captor = ArgumentCaptor.forClass(UserIdentity.class);
  136. verify(callbackContext).authenticate(captor.capture());
  137. UserIdentity value = captor.getValue();
  138. assertThat(value.getGroups()).contains("group1", "group2");
  139. assertThat(gitlab.takeRequest().getPath()).isEqualTo("/oauth/token");
  140. assertThat(gitlab.takeRequest().getPath()).isEqualTo("/api/v4/user");
  141. assertThat(gitlab.takeRequest().getPath()).isEqualTo("/api/v4/groups?min_access_level=10&per_page=100");
  142. }
  143. @Test
  144. public void callback_whenMultiplePagesOfGroups_shouldSynchronizeAllGroups() {
  145. mapSettings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "true");
  146. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  147. mockAccessTokenResponse();
  148. mockUserResponse();
  149. // Response for /groups, first page
  150. gitlab.enqueue(new MockResponse()
  151. .setBody("""
  152. [
  153. {
  154. "id": 1,
  155. "full_path": "group1"
  156. },
  157. {
  158. "id": 2,
  159. "full_path": "group2"
  160. }
  161. ]
  162. """)
  163. .setHeader("Link", format(" <%s/groups?per_page=100&page=2>; rel=\"next\"," +
  164. " <%s/groups?per_page=100&&page=3>; rel=\"last\"," +
  165. " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl, gitLabUrl)));
  166. // Response for /groups, page 2
  167. gitlab.enqueue(new MockResponse()
  168. .setBody("""
  169. [
  170. {
  171. "id": 3,
  172. "full_path": "group3"
  173. },
  174. {
  175. "id": 4,
  176. "full_path": "group4"
  177. }
  178. ]
  179. """)
  180. .setHeader("Link", format("<%s/groups?per_page=100&page=3>; rel=\"next\"," +
  181. " <%s/groups?per_page=100&&page=3>; rel=\"last\"," +
  182. " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl, gitLabUrl)));
  183. // Response for /groups, page 3
  184. gitlab.enqueue(new MockResponse()
  185. .setBody("""
  186. [
  187. {
  188. "id": 5,
  189. "full_path": "group5"
  190. },
  191. {
  192. "id": 6,
  193. "full_path": "group6"
  194. }
  195. ]
  196. """)
  197. .setHeader("Link", format("<%s/groups?per_page=100&&page=3>; rel=\"last\"," +
  198. " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl)));
  199. gitLabIdentityProvider.callback(callbackContext);
  200. ArgumentCaptor<UserIdentity> captor = ArgumentCaptor.forClass(UserIdentity.class);
  201. verify(callbackContext).authenticate(captor.capture());
  202. UserIdentity value = captor.getValue();
  203. assertThat(value.getGroups()).contains("group1", "group2", "group3", "group4", "group5", "group6");
  204. }
  205. @Test
  206. public void callback_whenNoUser_shouldThrow() {
  207. OAuth2IdentityProvider.CallbackContext callbackContext = mockCallbackContext();
  208. mockAccessTokenResponse();
  209. // Response for /user
  210. gitlab.enqueue(new MockResponse().setResponseCode(404).setBody("empty"));
  211. assertThatThrownBy(() -> gitLabIdentityProvider.callback(callbackContext))
  212. .hasMessage("Fail to execute request '" + gitLabSettings.url() + "/api/v4/user'. HTTP code: 404, response: empty")
  213. .isInstanceOf((IllegalStateException.class));
  214. }
  215. private static OAuth2IdentityProvider.CallbackContext mockCallbackContext() {
  216. OAuth2IdentityProvider.CallbackContext callbackContext = Mockito.mock(OAuth2IdentityProvider.CallbackContext.class);
  217. when(callbackContext.getCallbackUrl()).thenReturn("http://server/callback");
  218. HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class);
  219. when(httpServletRequest.getParameter("code")).thenReturn(ANY_CODE_VALUE);
  220. when(callbackContext.getRequest()).thenReturn(httpServletRequest);
  221. return callbackContext;
  222. }
  223. private void mockAccessTokenResponse() {
  224. // Response for OAuth access token
  225. gitlab.enqueue(new MockResponse().setBody("""
  226. {
  227. "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
  228. "token_type": "bearer",
  229. "expires_in": 7200,
  230. "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
  231. }
  232. """));
  233. }
  234. private void mockUserResponse() {
  235. // Response for /user
  236. gitlab.enqueue(new MockResponse().setBody("""
  237. {
  238. "id": 123,
  239. "username": "toto",
  240. "name": "Toto Toto",
  241. "email": "toto@toto.com"
  242. }
  243. """));
  244. }
  245. private void mockSingleGroupReponse(String group) {
  246. // Response for /groups
  247. gitlab.enqueue(new MockResponse().setBody("""
  248. [
  249. {
  250. "id": 1,
  251. "full_path": "%s"
  252. }
  253. ]
  254. """.formatted(group)));
  255. }
  256. }