package org.sonar.auth.gitlab;
import com.github.scribejava.core.builder.ServiceBuilder;
-import com.github.scribejava.core.builder.ServiceBuilderOAuth20;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthConstants;
import com.github.scribejava.core.oauth.OAuth20Service;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
+import javax.inject.Inject;
import org.sonar.api.server.authentication.Display;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
import org.sonar.api.server.authentication.UnauthorizedException;
public class GitLabIdentityProvider implements OAuth2IdentityProvider {
- public static final String API_SCOPE = "api";
- public static final String READ_USER_SCOPE = "read_user";
public static final String KEY = "gitlab";
private final GitLabSettings gitLabSettings;
private final ScribeGitLabOauth2Api scribeApi;
private final GitLabRestClient gitLabRestClient;
+ private final ScribeFactory scribeFactory;
+ @Inject
public GitLabIdentityProvider(GitLabSettings gitLabSettings, GitLabRestClient gitLabRestClient, ScribeGitLabOauth2Api scribeApi) {
+ this(gitLabSettings, gitLabRestClient, scribeApi, new ScribeFactory());
+ }
+
+ @VisibleForTesting
+ GitLabIdentityProvider(GitLabSettings gitLabSettings, GitLabRestClient gitLabRestClient, ScribeGitLabOauth2Api scribeApi,
+ ScribeFactory scribeFactory) {
this.gitLabSettings = gitLabSettings;
this.scribeApi = scribeApi;
this.gitLabRestClient = gitLabRestClient;
+ this.scribeFactory = scribeFactory;
}
@Override
@Override
public void init(InitContext context) {
String state = context.generateCsrfState();
- OAuth20Service scribe = newScribeBuilder(context).build(scribeApi);
- String url = scribe.getAuthorizationUrl(state);
- context.redirectTo(url);
- }
-
- private ServiceBuilderOAuth20 newScribeBuilder(OAuth2Context context) {
- checkState(isEnabled(), "GitLab authentication is disabled");
- return new ServiceBuilder(gitLabSettings.applicationId())
- .apiSecret(gitLabSettings.secret())
- .defaultScope(gitLabSettings.syncUserGroups() ? API_SCOPE : READ_USER_SCOPE)
- .callback(context.getCallbackUrl());
+ try (OAuth20Service scribe = scribeFactory.newScribe(gitLabSettings, context.getCallbackUrl(), scribeApi)) {
+ String url = scribe.getAuthorizationUrl(state);
+ context.redirectTo(url);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
}
@Override
public void callback(CallbackContext context) {
- try {
- onCallback(context);
+ try (OAuth20Service scribe = scribeFactory.newScribe(gitLabSettings, context.getCallbackUrl(), scribeApi)) {
+ onCallback(context, scribe);
} catch (IOException | ExecutionException e) {
throw new IllegalStateException(e);
} catch (InterruptedException e) {
}
}
- private void onCallback(CallbackContext context) throws InterruptedException, ExecutionException, IOException {
+ private void onCallback(CallbackContext context, OAuth20Service scribe) throws InterruptedException, ExecutionException, IOException {
HttpRequest request = context.getHttpRequest();
- OAuth20Service scribe = newScribeBuilder(context).build(scribeApi);
String code = request.getParameter(OAuthConstants.CODE);
OAuth2AccessToken accessToken = scribe.getAccessToken(code);
-
GsonUser user = gitLabRestClient.getUser(scribe, accessToken);
UserIdentity.Builder builder = UserIdentity.builder()
.setName(user.getName())
.setEmail(user.getEmail());
-
- Set<String> userGroups = getGroups(scribe, accessToken);
-
- if (!gitLabSettings.allowedGroups().isEmpty()) {
- validateUserInAllowedGroups(userGroups, gitLabSettings.allowedGroups());
- }
-
if (gitLabSettings.syncUserGroups()) {
+ Set<String> userGroups = getGroups(scribe, accessToken);
+ validateUserInAllowedGroups(userGroups, gitLabSettings.allowedGroups());
builder.setGroups(userGroups);
}
-
context.authenticate(builder.build());
context.redirectToRequestedPage();
}
- private static void validateUserInAllowedGroups(Set<String> userGroups, Set<String> allowedGroups) {
+ private void validateUserInAllowedGroups(Set<String> userGroups, Set<String> allowedGroups) {
+ if (gitLabSettings.allowedGroups().isEmpty()) {
+ return;
+ }
+
boolean allowedUser = userGroups.stream()
.anyMatch(userGroup -> isAllowedGroup(userGroup, allowedGroups));
.collect(toSet());
}
+ static class ScribeFactory {
+
+ private static final String API_SCOPE = "api";
+ private static final String READ_USER_SCOPE = "read_user";
+
+ OAuth20Service newScribe(GitLabSettings gitLabSettings, String callbackUrl, ScribeGitLabOauth2Api scribeApi) {
+ checkState(gitLabSettings.isEnabled(), "GitLab authentication is disabled");
+ return new ServiceBuilder(gitLabSettings.applicationId())
+ .apiSecret(gitLabSettings.secret())
+ .defaultScope(gitLabSettings.syncUserGroups() ? API_SCOPE : READ_USER_SCOPE)
+ .callback(callbackUrl)
+ .build(scribeApi);
+ }
+ }
+
}
*/
package org.sonar.auth.gitlab;
-import org.assertj.core.api.Assertions;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuthConstants;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
import org.sonar.api.server.authentication.Display;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UnauthorizedException;
+import org.sonar.api.server.authentication.UserIdentity;
+import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.openMocks;
+@RunWith(DataProviderRunner.class)
public class GitLabIdentityProviderTest {
+ private static final String OAUTH_CODE = "code fdsojfsjodfg";
+ private static final String AUTHORIZATION_URL = "AUTHORIZATION_URL";
+ private static final String CALLBACK_URL = "CALLBACK_URL";
+ private static final String STATE = "State request";
+
+ @Mock
+ private GitLabRestClient gitLabRestClient;
+ @Mock
+ private GitLabSettings gitLabSettings;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private GitLabIdentityProvider.ScribeFactory scribeFactory;
+ @Mock
+ private OAuth2IdentityProvider.InitContext initContext;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private OAuth2IdentityProvider.CallbackContext callbackContext;
+ @Mock
+ private OAuth20Service scribe;
+ @Mock
+ private ScribeGitLabOauth2Api scribeApi;
+ @Mock
+ private OAuth2AccessToken accessToken;
+
+ private GitLabIdentityProvider gitLabIdentityProvider;
+
+ @Before
+ public void setup() throws IOException, ExecutionException, InterruptedException {
+ openMocks(this);
+ gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, gitLabRestClient, scribeApi, scribeFactory);
+
+ when(initContext.generateCsrfState()).thenReturn(STATE);
+ when(initContext.getCallbackUrl()).thenReturn(CALLBACK_URL);
+
+ when(callbackContext.getCallbackUrl()).thenReturn(CALLBACK_URL);
+ when(callbackContext.getHttpRequest().getParameter(OAuthConstants.CODE)).thenReturn(OAUTH_CODE);
+
+ when(scribeFactory.newScribe(gitLabSettings, CALLBACK_URL, scribeApi)).thenReturn(scribe);
+ when(scribe.getAccessToken(OAUTH_CODE)).thenReturn(accessToken);
+ when(scribe.getAuthorizationUrl(STATE)).thenReturn(AUTHORIZATION_URL);
+ }
+
@Test
public void test_identity_provider() {
- GitLabSettings gitLabSettings = mock(GitLabSettings.class);
when(gitLabSettings.isEnabled()).thenReturn(true);
when(gitLabSettings.allowUsersToSignUp()).thenReturn(true);
- GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, new GitLabRestClient(gitLabSettings),
- new ScribeGitLabOauth2Api(gitLabSettings));
assertThat(gitLabIdentityProvider.getKey()).isEqualTo("gitlab");
assertThat(gitLabIdentityProvider.getName()).isEqualTo("GitLab");
}
@Test
- public void test_init() {
- GitLabSettings gitLabSettings = mock(GitLabSettings.class);
- when(gitLabSettings.isEnabled()).thenReturn(true);
- when(gitLabSettings.allowUsersToSignUp()).thenReturn(true);
- when(gitLabSettings.applicationId()).thenReturn("123");
- when(gitLabSettings.secret()).thenReturn("456");
- when(gitLabSettings.url()).thenReturn("http://server");
- when(gitLabSettings.syncUserGroups()).thenReturn(true);
- GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, new GitLabRestClient(gitLabSettings),
- new ScribeGitLabOauth2Api(gitLabSettings));
+ public void init_whenSuccessful_redirectsToUrl() {
+ gitLabIdentityProvider.init(initContext);
+
+ verify(initContext).generateCsrfState();
+ verify(initContext).redirectTo(AUTHORIZATION_URL);
+ }
+
+ @Test
+ public void init_whenErrorWhileBuildingScribe_shouldReThrow() {
+ IllegalStateException exception = new IllegalStateException("GitLab authentication is disabled");
+ when(scribeFactory.newScribe(any(), any(), any())).thenThrow(exception);
OAuth2IdentityProvider.InitContext initContext = mock(OAuth2IdentityProvider.InitContext.class);
when(initContext.getCallbackUrl()).thenReturn("http://server/callback");
- gitLabIdentityProvider.init(initContext);
+ assertThatIllegalStateException()
+ .isThrownBy(() -> gitLabIdentityProvider.init(initContext))
+ .isEqualTo(exception);
+ }
+
+ @Test
+ public void onCallback_withGroupSyncDisabledAndNoAllowedGroups_redirectsToRequestedPage() {
+ GsonUser gsonUser = mockGsonUser();
+
+ gitLabIdentityProvider.callback(callbackContext);
- verify(initContext).redirectTo("http://server/oauth/authorize?response_type=code&client_id=123&redirect_uri=http%3A%2F%2Fserver%2Fcallback&scope=api");
+ verifyAuthenticateIsCalledWithExpectedIdentity(callbackContext, gsonUser, Set.of());
+ verify(callbackContext).redirectToRequestedPage();
+ verify(gitLabRestClient, never()).getGroups(any(), any());
}
@Test
- public void test_init_without_sync() {
- GitLabSettings gitLabSettings = mock(GitLabSettings.class);
- when(gitLabSettings.isEnabled()).thenReturn(true);
- when(gitLabSettings.allowUsersToSignUp()).thenReturn(true);
- when(gitLabSettings.applicationId()).thenReturn("123");
- when(gitLabSettings.secret()).thenReturn("456");
- when(gitLabSettings.url()).thenReturn("http://server");
+ public void onCallback_withGroupSyncDisabledAndAllowedGroups_redirectsToRequestedPage() {
when(gitLabSettings.syncUserGroups()).thenReturn(false);
- GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, new GitLabRestClient(gitLabSettings),
- new ScribeGitLabOauth2Api(gitLabSettings));
- OAuth2IdentityProvider.InitContext initContext = mock(OAuth2IdentityProvider.InitContext.class);
- when(initContext.getCallbackUrl()).thenReturn("http://server/callback");
+ GsonUser gsonUser = mockGsonUser();
- gitLabIdentityProvider.init(initContext);
+ gitLabIdentityProvider.callback(callbackContext);
- verify(initContext).redirectTo("http://server/oauth/authorize?response_type=code&client_id=123&redirect_uri=http%3A%2F%2Fserver%2Fcallback&scope=read_user");
+ verifyAuthenticateIsCalledWithExpectedIdentity(callbackContext, gsonUser, Set.of());
+ verify(callbackContext).redirectToRequestedPage();
+ verify(gitLabRestClient, never()).getGroups(any(), any());
}
@Test
- public void fail_to_init() {
- GitLabSettings gitLabSettings = mock(GitLabSettings.class);
+ @UseDataProvider("allowedGroups")
+ public void onCallback_withGroupSyncAndAllowedGroupsMatching_redirectsToRequestedPage(Set<String> allowedGroups) {
+ when(gitLabSettings.syncUserGroups()).thenReturn(true);
+ when(gitLabSettings.allowedGroups()).thenReturn(allowedGroups);
+
+ GsonUser gsonUser = mockGsonUser();
+ Set<GsonGroup> gsonGroups = mockGitlabGroups();
+
+ gitLabIdentityProvider.callback(callbackContext);
+
+ verifyAuthenticateIsCalledWithExpectedIdentity(callbackContext, gsonUser, gsonGroups);
+ verify(callbackContext).redirectToRequestedPage();
+ }
+
+ @DataProvider
+ public static Object[][] allowedGroups() {
+ return new Object[][]{
+ {Set.of()},
+ {Set.of("path")}
+ };
+ }
+
+ @Test
+ public void onCallback_withGroupSyncAndAllowedGroupsNotMatching_shouldThrow() {
+ when(gitLabSettings.syncUserGroups()).thenReturn(true);
+ when(gitLabSettings.allowedGroups()).thenReturn(Set.of("path2"));
+
+ mockGsonUser();
+ mockGitlabGroups();
+
+ assertThatExceptionOfType(UnauthorizedException.class)
+ .isThrownBy(() -> gitLabIdentityProvider.callback(callbackContext))
+ .withMessage("You are not allowed to authenticate");
+ }
+
+ @Test
+ public void onCallback_ifScribeFactoryFails_shouldThrow() {
+ IllegalStateException exception = new IllegalStateException("message");
+ when(scribeFactory.newScribe(any(), any(), any())).thenThrow(exception);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> gitLabIdentityProvider.callback(callbackContext))
+ .isEqualTo(exception);
+ }
+
+ private Set<GsonGroup> mockGitlabGroups() {
+ GsonGroup gsonGroup = mock(GsonGroup.class);
+ when(gsonGroup.getFullPath()).thenReturn("path/to/group");
+ GsonGroup gsonGroup2 = mock(GsonGroup.class);
+ when(gsonGroup2.getFullPath()).thenReturn("path/to/group2");
+ when(gitLabRestClient.getGroups(scribe, accessToken)).thenReturn(List.of(gsonGroup, gsonGroup2));
+ return Set.of(gsonGroup, gsonGroup2);
+ }
+
+ private static void verifyAuthenticateIsCalledWithExpectedIdentity(OAuth2IdentityProvider.CallbackContext callbackContext,
+ GsonUser gsonUser, Set<GsonGroup> gsonGroups) {
+ ArgumentCaptor<UserIdentity> userIdentityCaptor = ArgumentCaptor.forClass(UserIdentity.class);
+ verify(callbackContext).authenticate(userIdentityCaptor.capture());
+
+ UserIdentity actualIdentity = userIdentityCaptor.getValue();
+
+ assertThat(actualIdentity.getProviderId()).asLong().isEqualTo(gsonUser.getId());
+ assertThat(actualIdentity.getProviderLogin()).isEqualTo(gsonUser.getUsername());
+ assertThat(actualIdentity.getName()).isEqualTo(gsonUser.getName());
+ assertThat(actualIdentity.getEmail()).isEqualTo(gsonUser.getEmail());
+ assertThat(actualIdentity.getGroups()).isEqualTo(gsonGroups.stream().map(GsonGroup::getFullPath).collect(toSet()));
+ }
+
+ private GsonUser mockGsonUser() {
+ GsonUser gsonUser = mock();
+ when(gsonUser.getId()).thenReturn(432423L);
+ when(gsonUser.getUsername()).thenReturn("userName");
+ when(gsonUser.getName()).thenReturn("name");
+ when(gsonUser.getEmail()).thenReturn("toto@gitlab.com");
+ when(gitLabRestClient.getUser(scribe, accessToken)).thenReturn(gsonUser);
+ return gsonUser;
+ }
+
+ @Test
+ public void newScribe_whenGitLabAuthIsDisabled_throws() {
when(gitLabSettings.isEnabled()).thenReturn(false);
- when(gitLabSettings.allowUsersToSignUp()).thenReturn(true);
- when(gitLabSettings.applicationId()).thenReturn("123");
- when(gitLabSettings.secret()).thenReturn("456");
- when(gitLabSettings.url()).thenReturn("http://server");
- GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, new GitLabRestClient(gitLabSettings),
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> new GitLabIdentityProvider.ScribeFactory().newScribe(gitLabSettings, CALLBACK_URL, new ScribeGitLabOauth2Api(gitLabSettings)))
+ .withMessage("GitLab authentication is disabled");
+ }
+
+ @Test
+ @UseDataProvider("groupsSyncToScope")
+ public void newScribe_whenGitLabSettingsValid_shouldUseCorrectScopeDependingOnGroupSync(boolean groupSyncEnabled, String expectedScope) {
+ setupGitlabSettingsWithGroupSync(groupSyncEnabled);
+
+
+ OAuth20Service realScribe = new GitLabIdentityProvider.ScribeFactory().newScribe(gitLabSettings, CALLBACK_URL,
new ScribeGitLabOauth2Api(gitLabSettings));
- OAuth2IdentityProvider.InitContext initContext = mock(OAuth2IdentityProvider.InitContext.class);
- when(initContext.getCallbackUrl()).thenReturn("http://server/callback");
+ assertThat(realScribe).isNotNull();
+ assertThat(realScribe.getCallback()).isEqualTo(CALLBACK_URL);
+ assertThat(realScribe.getApiSecret()).isEqualTo(gitLabSettings.secret());
+ assertThat(realScribe.getDefaultScope()).isEqualTo(expectedScope);
+ }
- Assertions.assertThatThrownBy(() -> gitLabIdentityProvider.init(initContext))
- .hasMessage("GitLab authentication is disabled")
- .isInstanceOf(IllegalStateException.class);
+ @DataProvider
+ public static Object[][] groupsSyncToScope() {
+ return new Object[][]{
+ {false, "read_user"},
+ {true, "api"}
+ };
+ }
+
+ private void setupGitlabSettingsWithGroupSync(boolean enableGroupSync) {
+ when(gitLabSettings.isEnabled()).thenReturn(true);
+ when(gitLabSettings.applicationId()).thenReturn("123");
+ when(gitLabSettings.secret()).thenReturn("456");
+ when(gitLabSettings.syncUserGroups()).thenReturn(enableGroupSync);
}
}