diff options
author | Julien Lancelot <julien.lancelot@sonarsource.com> | 2019-09-17 12:21:53 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-23 20:21:07 +0200 |
commit | 056302d0d534a30a8a7d905602455b0ba0f900a2 (patch) | |
tree | 1e755215fb861b215e97e724239c22036cccc639 /server | |
parent | 26a7d44c9cce85b62e6d3f2043aa3eddc500a54e (diff) | |
download | sonarqube-056302d0d534a30a8a7d905602455b0ba0f900a2.tar.gz sonarqube-056302d0d534a30a8a7d905602455b0ba0f900a2.zip |
SONAR-12460 Support GitLab Authentication
Diffstat (limited to 'server')
18 files changed, 1146 insertions, 0 deletions
diff --git a/server/sonar-auth-gitlab/build.gradle b/server/sonar-auth-gitlab/build.gradle new file mode 100644 index 00000000000..f30f08323b0 --- /dev/null +++ b/server/sonar-auth-gitlab/build.gradle @@ -0,0 +1,25 @@ +description = 'SonarQube :: Authentication :: GitLab' + +configurations { + testCompile.extendsFrom compileOnly +} + +dependencies { + // please keep the list ordered + + compile 'com.github.scribejava:scribejava-apis' + compile 'com.github.scribejava:scribejava-core' + compile 'com.google.code.gson:gson' + + compileOnly 'com.google.code.findbugs:jsr305' + compileOnly 'com.squareup.okhttp3:okhttp' + compileOnly 'javax.servlet:javax.servlet-api' + compileOnly project(':sonar-core') + compileOnly project(':sonar-ws') + + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'com.squareup.okhttp3:okhttp' + testCompile 'junit:junit' + testCompile 'org.assertj:assertj-core' + testCompile 'org.mockito:mockito-core' +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java new file mode 100644 index 00000000000..5921bac826b --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java @@ -0,0 +1,135 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthConstants; +import com.github.scribejava.core.oauth.OAuth20Service; +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.servlet.http.HttpServletRequest; +import org.sonar.api.server.authentication.Display; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; +import org.sonar.api.server.authentication.UserIdentity; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.stream.Collectors.toSet; + +public class GitLabIdentityProvider implements OAuth2IdentityProvider { + + private final GitLabSettings gitLabSettings; + private final ScribeGitLabOauth2Api scribeApi; + private final GitLabRestClient gitLabRestClient; + + public GitLabIdentityProvider(GitLabSettings gitLabSettings, GitLabRestClient gitLabRestClient, ScribeGitLabOauth2Api scribeApi) { + this.gitLabSettings = gitLabSettings; + this.scribeApi = scribeApi; + this.gitLabRestClient = gitLabRestClient; + } + + @Override + public String getKey() { + return "gitlab"; + } + + @Override + public String getName() { + return "GitLab"; + } + + @Override + public Display getDisplay() { + return Display.builder() + .setIconPath("/images/gitlab-icon-rgb.svg").setBackgroundColor("#6a4fbb").build(); + } + + @Override + public boolean isEnabled() { + return gitLabSettings.isEnabled(); + } + + @Override + public boolean allowsUsersToSignUp() { + return gitLabSettings.allowUsersToSignUp(); + } + + @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 ServiceBuilder newScribeBuilder(OAuth2Context context) { + checkState(isEnabled(), "GitLab authentication is disabled"); + return new ServiceBuilder(gitLabSettings.applicationId()) + .apiSecret(gitLabSettings.secret()) + .callback(context.getCallbackUrl()); + } + + @Override + public void callback(CallbackContext context) { + try { + onCallback(context); + } catch (IOException | ExecutionException e) { + throw new IllegalStateException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + private void onCallback(CallbackContext context) throws InterruptedException, ExecutionException, IOException { + HttpServletRequest request = context.getRequest(); + 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() + .setProviderId(Long.toString(user.getId())) + .setProviderLogin(user.getUsername()) + .setName(user.getName()) + .setEmail(user.getEmail()); + + if (gitLabSettings.syncUserGroups()) { + builder.setGroups(getGroups(scribe, accessToken)); + } + + context.authenticate(builder.build()); + context.redirectToRequestedPage(); + } + + private Set<String> getGroups(OAuth20Service scribe, OAuth2AccessToken accessToken) { + List<GsonGroup> groups = gitLabRestClient.getGroups(scribe, accessToken); + return Stream.of(groups) + .flatMap(Collection::stream) + .map(GsonGroup::getFullPath) + .collect(toSet()); + } + +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabModule.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabModule.java new file mode 100644 index 00000000000..884dd1741c5 --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabModule.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import java.util.List; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.core.platform.Module; + +import static org.sonar.auth.gitlab.GitLabSettings.definitions; + +public class GitLabModule extends Module { + + @Override + protected void configureModule() { + add( + GitLabIdentityProvider.class, + GitLabRestClient.class, + GitLabSettings.class, + ScribeGitLabOauth2Api.class); + List<PropertyDefinition> definitions = definitions(); + add(definitions.toArray(new Object[definitions.size()])); + } + +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java new file mode 100644 index 00000000000..9abfcbe35ac --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; + +public class GitLabRestClient { + + private static final int DEFAULT_PAGE_SIZE = 100; + private static final Pattern NEXT_LINK_PATTERN = Pattern.compile(".*<(.*)>; rel=\"next\""); + + private static final String API_SUFFIX = "/api/v4"; + + private final GitLabSettings settings; + + public GitLabRestClient(GitLabSettings settings) { + this.settings = settings; + } + + GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) { + try (Response response = executeRequest(settings.url() + API_SUFFIX + "/user", scribe, accessToken)) { + String responseBody = response.getBody(); + return GsonUser.parse(responseBody); + } catch (IOException e) { + throw new IllegalStateException("Failed to get gitlab user", e); + } + } + + List<GsonGroup> getGroups(OAuth20Service scribe, OAuth2AccessToken accessToken) { + return executePaginatedQuery(settings.url() + API_SUFFIX + "/groups", scribe, accessToken, GsonGroup::parse); + } + + private static Response executeRequest(String requestUrl, OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException { + OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl); + scribe.signRequest(accessToken, request); + try { + Response response = scribe.execute(request); + if (!response.isSuccessful()) { + throw unexpectedResponseCode(requestUrl, response); + } + return response; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } catch (ExecutionException e) { + throw new IllegalStateException(e); + } + } + + private static <E> List<E> executePaginatedQuery(String query, OAuth20Service scribe, OAuth2AccessToken accessToken, Function<String, List<E>> function) { + List<E> result = new ArrayList<>(); + readNextPage(result, scribe, accessToken, query + "?per_page=" + DEFAULT_PAGE_SIZE, function); + return result; + } + + private static <E> void readNextPage(List<E> result, OAuth20Service scribe, OAuth2AccessToken accessToken, String nextEndPoint, Function<String, List<E>> function) { + try (Response nextResponse = executeRequest(nextEndPoint, scribe, accessToken)) { + String content = nextResponse.getBody(); + if (content == null) { + return; + } + result.addAll(function.apply(content)); + readNextEndPoint(nextResponse).ifPresent(newNextEndPoint -> readNextPage(result, scribe, accessToken, newNextEndPoint, function)); + } catch (IOException e) { + throw new IllegalStateException(format("Failed to get %s", nextEndPoint), e); + } + } + + private static Optional<String> readNextEndPoint(Response response) { + String link = response.getHeader("Link"); + if (link == null || link.isEmpty() || !link.contains("rel=\"next\"")) { + return Optional.empty(); + } + Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(link); + if (!nextLinkMatcher.find()) { + return Optional.empty(); + } + return Optional.of(nextLinkMatcher.group(1)); + } + + private static IllegalStateException unexpectedResponseCode(String requestUrl, Response response) throws IOException { + return new IllegalStateException(format("Fail to execute request '%s'. HTTP code: %s, response: %s", requestUrl, response.getCode(), response.getBody())); + } + +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabSettings.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabSettings.java new file mode 100644 index 00000000000..7eb6c23670f --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabSettings.java @@ -0,0 +1,127 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import java.util.Arrays; +import java.util.List; +import org.sonar.api.CoreProperties; +import org.sonar.api.PropertyType; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.PropertyDefinition; + +import static java.lang.String.valueOf; +import static org.sonar.api.PropertyType.BOOLEAN; + +public class GitLabSettings { + + static final String GITLAB_AUTH_ENABLED = "sonar.auth.gitlab.enabled"; + static final String GITLAB_AUTH_URL = "sonar.auth.gitlab.url"; + static final String GITLAB_AUTH_APPLICATION_ID = "sonar.auth.gitlab.applicationId"; + static final String GITLAB_AUTH_SECRET = "sonar.auth.gitlab.secret"; + static final String GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP = "sonar.auth.gitlab.allowUsersToSignUp"; + static final String GITLAB_AUTH_SYNC_USER_GROUPS = "sonar.auth.gitlab.groupsSync"; + + private static final String CATEGORY = CoreProperties.CATEGORY_SECURITY; + private static final String SUBCATEGORY = "gitlab"; + + private final Configuration configuration; + + public GitLabSettings(Configuration configuration) { + this.configuration = configuration; + } + + public String url() { + return configuration.get(GITLAB_AUTH_URL).orElse(null); + } + + public String applicationId() { + return configuration.get(GITLAB_AUTH_APPLICATION_ID).orElse(null); + } + + public String secret() { + return configuration.get(GITLAB_AUTH_SECRET).orElse(null); + } + + public boolean isEnabled() { + return configuration.getBoolean(GITLAB_AUTH_ENABLED).orElse(false) && applicationId() != null && secret() != null; + } + + public boolean allowUsersToSignUp() { + return configuration.getBoolean(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP).orElse(false); + } + + public boolean syncUserGroups() { + return configuration.getBoolean(GITLAB_AUTH_SYNC_USER_GROUPS).orElse(false); + } + + static List<PropertyDefinition> definitions() { + return Arrays.asList( + PropertyDefinition.builder(GITLAB_AUTH_ENABLED) + .name("Enabled") + .description("Enable Gitlab users to login. Value is ignored if URL, Application ID, and Secret are not set.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .type(BOOLEAN) + .defaultValue(valueOf(false)) + .index(1) + .build(), + PropertyDefinition.builder(GITLAB_AUTH_URL) + .name("GitLab URL") + .description("URL to access GitLab.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .defaultValue("https://gitlab.com") + .index(2) + .build(), + PropertyDefinition.builder(GITLAB_AUTH_APPLICATION_ID) + .name("Application ID") + .description("Application ID provided by GitLab when registering the application.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .index(3) + .build(), + PropertyDefinition.builder(GITLAB_AUTH_SECRET) + .name("Secret") + .description("Secret provided by GitLab when registering the application.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .index(4) + .build(), + PropertyDefinition.builder(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP) + .name("Allow users to sign-up") + .description("Allow new users to authenticate. When set to 'false', only existing users will be able to authenticate to the server.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .type(BOOLEAN) + .defaultValue(valueOf(true)) + .index(5) + .build(), + PropertyDefinition.builder(GITLAB_AUTH_SYNC_USER_GROUPS) + .deprecatedKey("sonar.auth.gitlab.sync_user_groups") + .name("Synchronize user groups") + .description("For each GitLab group he belongs to, the user will be associated to a group with the same name (if it exists) in SonarQube.") + .category(CATEGORY) + .subCategory(SUBCATEGORY) + .type(PropertyType.BOOLEAN) + .defaultValue(valueOf(false)) + .index(6) + .build()); + } +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java new file mode 100644 index 00000000000..b1e6c883d09 --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; + +/** + * Lite representation of JSON response of GET https://docs.gitlab.com/ee/api/groups.html + */ +public class GsonGroup { + + private String full_path; + + public GsonGroup() { + // http://stackoverflow.com/a/18645370/229031 + this(""); + } + + GsonGroup(String full_path) { + this.full_path = full_path; + } + + String getFullPath() { + return full_path; + } + + static List<GsonGroup> parse(String json) { + Type collectionType = new TypeToken<Collection<GsonGroup>>() { + }.getType(); + Gson gson = new Gson(); + return gson.fromJson(json, collectionType); + } + +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonUser.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonUser.java new file mode 100644 index 00000000000..b7d4e0e6825 --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonUser.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import com.google.gson.Gson; + +/** + * Lite representation of JSON response of GET https://gitlab.com/api/v4/user + */ +public class GsonUser { + private long id; + private String username; + private String name; + private String email; + + public long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public static GsonUser parse(String json) { + Gson gson = new Gson(); + return gson.fromJson(json, GsonUser.class); + } +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/ScribeGitLabOauth2Api.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/ScribeGitLabOauth2Api.java new file mode 100644 index 00000000000..0b15a060751 --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/ScribeGitLabOauth2Api.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import com.github.scribejava.core.builder.api.DefaultApi20; + +public class ScribeGitLabOauth2Api extends DefaultApi20 { + + private final GitLabSettings settings; + + public ScribeGitLabOauth2Api(GitLabSettings settings) { + this.settings = settings; + } + + @Override + public String getAccessTokenEndpoint() { + return settings.url() + "/oauth/token"; + } + + @Override + protected String getAuthorizationBaseUrl() { + return settings.url() + "/oauth/authorize"; + } + +} diff --git a/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/package-info.java b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/package-info.java new file mode 100644 index 00000000000..31e176a0e15 --- /dev/null +++ b/server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.auth.gitlab; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabIdentityProviderTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabIdentityProviderTest.java new file mode 100644 index 00000000000..9558f8a65e0 --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabIdentityProviderTest.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.authentication.Display; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class GitLabIdentityProviderTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @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"); + Display display = gitLabIdentityProvider.getDisplay(); + assertThat(display.getIconPath()).isEqualTo("/images/gitlab-icon-rgb.svg"); + assertThat(display.getBackgroundColor()).isEqualTo("#6a4fbb"); + assertThat(gitLabIdentityProvider.isEnabled()).isTrue(); + assertThat(gitLabIdentityProvider.allowsUsersToSignUp()).isTrue(); + } + + @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"); + 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"); + + gitLabIdentityProvider.init(initContext); + + verify(initContext).redirectTo("http://server/oauth/authorize?response_type=code&client_id=123&redirect_uri=http%3A%2F%2Fserver%2Fcallback"); + } + + @Test + public void fail_to_init() { + GitLabSettings gitLabSettings = mock(GitLabSettings.class); + 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), + new ScribeGitLabOauth2Api(gitLabSettings)); + + OAuth2IdentityProvider.InitContext initContext = mock(OAuth2IdentityProvider.InitContext.class); + when(initContext.getCallbackUrl()).thenReturn("http://server/callback"); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("GitLab authentication is disabled"); + + gitLabIdentityProvider.init(initContext); + } +} diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabModuleTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabModuleTest.java new file mode 100644 index 00000000000..b905f7eb8ed --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabModuleTest.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class GitLabModuleTest { + + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new GitLabModule().configure(container); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 10); + } + +} diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabSettingsTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabSettingsTest.java new file mode 100644 index 00000000000..b73fe1826a2 --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabSettingsTest.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.internal.MapSettings; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL; + +public class GitLabSettingsTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private MapSettings settings; + private GitLabSettings config; + + @Before + public void prepare() { + settings = new MapSettings(new PropertyDefinitions(GitLabSettings.definitions())); + config = new GitLabSettings(settings.asConfig()); + } + + @Test + public void test_settings() { + assertThat(config.url()).isEqualTo("https://gitlab.com"); + settings.setProperty(GITLAB_AUTH_URL, "https://gitlab.com/api"); + assertThat(config.url()).isEqualTo("https://gitlab.com/api"); + + assertThat(config.isEnabled()).isFalse(); + settings.setProperty(GITLAB_AUTH_ENABLED, "true"); + assertThat(config.isEnabled()).isFalse(); + settings.setProperty(GITLAB_AUTH_APPLICATION_ID, "1234"); + assertThat(config.isEnabled()).isFalse(); + settings.setProperty(GITLAB_AUTH_SECRET, "5678"); + assertThat(config.isEnabled()).isTrue(); + + assertThat(config.applicationId()).isEqualTo("1234"); + assertThat(config.secret()).isEqualTo("5678"); + + assertThat(config.allowUsersToSignUp()).isTrue(); + settings.setProperty(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, "false"); + assertThat(config.allowUsersToSignUp()).isFalse(); + + assertThat(config.syncUserGroups()).isFalse(); + settings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, true); + assertThat(config.syncUserGroups()).isTrue(); + } +} diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java new file mode 100644 index 00000000000..a1605e1c9d6 --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import java.util.List; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GsonGroupTest { + + @Test + public void test_parse() { + List<GsonGroup> groups = GsonGroup.parse("[{\n" + + "\"id\": 123456789,\n" + + "\"web_url\": \"https://gitlab.com/groups/my-awesome-group/my-project\",\n" + + "\"name\": \"my-project\",\n" + + "\"path\": \"my-project\",\n" + + "\"description\": \"\",\n" + + "\"visibility\": \"private\",\n" + + "\"lfs_enabled\": true,\n" + + "\"avatar_url\": null,\n" + + "\"request_access_enabled\": false,\n" + + "\"full_name\": \"my-awesome-group / my-project\",\n" + + "\"full_path\": \"my-awesome-group/my-project\",\n" + + "\"parent_id\": 987654321,\n" + + "\"ldap_cn\": null,\n" + + "\"ldap_access\": null\n" + + "}]"); + + assertThat(groups).isNotNull(); + assertThat(groups.size()).isEqualTo(1); + assertThat(groups.get(0).getFullPath()).isEqualTo("my-awesome-group/my-project"); + } +} diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonUserTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonUserTest.java new file mode 100644 index 00000000000..79743f62a56 --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonUserTest.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GsonUserTest { + + @Test + public void test_parse() { + GsonUser gsonUser = GsonUser.parse("{\n" + + "\"id\": 4418804,\n" + + "\"name\": \"Pierre Guillot\",\n" + + "\"username\": \"pierre-guillot-sonarsource\",\n" + + "\"state\": \"active\",\n" + + "\"avatar_url\": \"https://secure.gravatar.com/avatar/fe075537af1b94fd1cea160e5359e178?s=80&d=identicon\",\n" + + "\"web_url\": \"https://gitlab.com/pierre-guillot-sonarsource\",\n" + + "\"created_at\": \"2019-08-06T08:36:09.031Z\",\n" + + "\"bio\": null,\n" + + "\"location\": null,\n" + + "\"public_email\": \"\",\n" + + "\"skype\": \"\",\n" + + "\"linkedin\": \"\",\n" + + "\"twitter\": \"\",\n" + + "\"website_url\": \"\",\n" + + "\"organization\": null,\n" + + "\"last_sign_in_at\": \"2019-08-19T11:53:15.041Z\",\n" + + "\"confirmed_at\": \"2019-08-06T08:36:08.246Z\",\n" + + "\"last_activity_on\": \"2019-08-23\",\n" + + "\"email\": \"pierre.guillot@sonarsource.com\",\n" + + "\"theme_id\": 1,\n" + + "\"color_scheme_id\": 1,\n" + + "\"projects_limit\": 100000,\n" + + "\"current_sign_in_at\": \"2019-08-23T09:27:42.853Z\",\n" + + "\"identities\": [\n" + + "{\n" + + "\"provider\": \"github\",\n" + + "\"extern_uid\": \"50145663\",\n" + + "\"saml_provider_id\": null\n" + + "}\n" + + "],\n" + + "\"can_create_group\": true,\n" + + "\"can_create_project\": true,\n" + + "\"two_factor_enabled\": false,\n" + + "\"external\": false,\n" + + "\"private_profile\": false,\n" + + "\"shared_runners_minutes_limit\": 50000,\n" + + "\"extra_shared_runners_minutes_limit\": null\n" + + "}"); + + assertThat(gsonUser).isNotNull(); + assertThat(gsonUser.getUsername()).isEqualTo("pierre-guillot-sonarsource"); + assertThat(gsonUser.getName()).isEqualTo("Pierre Guillot"); + assertThat(gsonUser.getEmail()).isEqualTo("pierre.guillot@sonarsource.com"); + } +} diff --git a/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/IntegrationTest.java b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/IntegrationTest.java new file mode 100644 index 00000000000..158d8457910 --- /dev/null +++ b/server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/IntegrationTest.java @@ -0,0 +1,192 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.gitlab; + +import javax.servlet.http.HttpServletRequest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.server.authentication.OAuth2IdentityProvider; +import org.sonar.api.server.authentication.UserIdentity; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_APPLICATION_ID; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_ENABLED; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SECRET; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_SYNC_USER_GROUPS; +import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL; + +public class IntegrationTest { + + private static final String ANY_CODE_VALUE = "ANY_CODE"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public MockWebServer gitlab = new MockWebServer(); + + private MapSettings mapSettings = new MapSettings(); + + private GitLabSettings gitLabSettings = new GitLabSettings(mapSettings.asConfig()); + + private String gitLabUrl; + + private GitLabIdentityProvider gitLabIdentityProvider = new GitLabIdentityProvider(gitLabSettings, + new GitLabRestClient(gitLabSettings), + new ScribeGitLabOauth2Api(gitLabSettings)); + + @Before + public void setUp() { + this.gitLabUrl = format("http://%s:%d", gitlab.getHostName(), gitlab.getPort()); + mapSettings + .setProperty(GITLAB_AUTH_ENABLED, "true") + .setProperty(GITLAB_AUTH_ALLOW_USERS_TO_SIGNUP, "true") + .setProperty(GITLAB_AUTH_URL, gitLabUrl) + .setProperty(GITLAB_AUTH_APPLICATION_ID, "123") + .setProperty(GITLAB_AUTH_SECRET, "456"); + } + + @Test + public void authenticate_user() { + OAuth2IdentityProvider.CallbackContext callbackContext = Mockito.mock(OAuth2IdentityProvider.CallbackContext.class); + when(callbackContext.getCallbackUrl()).thenReturn("http://server/callback"); + + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + when(httpServletRequest.getParameter("code")).thenReturn(ANY_CODE_VALUE); + when(callbackContext.getRequest()).thenReturn(httpServletRequest); + + gitlab.enqueue(new MockResponse().setBody( + "{\n" + " \"access_token\": \"de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54\",\n" + " \"token_type\": \"bearer\",\n" + " \"expires_in\": 7200,\n" + + " \"refresh_token\": \"8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1\"\n" + "}")); + // response of /user + gitlab.enqueue(new MockResponse().setBody("{\"id\": 123, \"username\":\"toto\", \"name\":\"Toto Toto\",\"email\":\"toto@toto.com\"}")); + + gitLabIdentityProvider.callback(callbackContext); + + ArgumentCaptor<UserIdentity> argument = ArgumentCaptor.forClass(UserIdentity.class); + verify(callbackContext).authenticate(argument.capture()); + assertThat(argument.getValue()).isNotNull(); + assertThat(argument.getValue().getLogin()).isNull(); + assertThat(argument.getValue().getProviderId()).isEqualTo("123"); + assertThat(argument.getValue().getProviderLogin()).isEqualTo("toto"); + assertThat(argument.getValue().getName()).isEqualTo("Toto Toto"); + assertThat(argument.getValue().getEmail()).isEqualTo("toto@toto.com"); + verify(callbackContext).redirectToRequestedPage(); + } + + @Test + public void synchronize_groups() { + mapSettings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "true"); + OAuth2IdentityProvider.CallbackContext callbackContext = Mockito.mock(OAuth2IdentityProvider.CallbackContext.class); + when(callbackContext.getCallbackUrl()).thenReturn("http://server/callback"); + + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + when(httpServletRequest.getParameter("code")).thenReturn(ANY_CODE_VALUE); + when(callbackContext.getRequest()).thenReturn(httpServletRequest); + + gitlab.enqueue(new MockResponse().setBody( + "{\n" + " \"access_token\": \"de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54\",\n" + " \"token_type\": \"bearer\",\n" + " \"expires_in\": 7200,\n" + + " \"refresh_token\": \"8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1\"\n" + "}")); + // response of /user + gitlab.enqueue(new MockResponse().setBody("{\"id\": 123, \"username\": \"username\", \"name\": \"name\"}")); + // response of /groups + gitlab.enqueue(new MockResponse().setBody("[{\"full_path\": \"group1\"}, {\"full_path\": \"group2\"}]")); + + gitLabIdentityProvider.callback(callbackContext); + + ArgumentCaptor<UserIdentity> captor = ArgumentCaptor.forClass(UserIdentity.class); + verify(callbackContext).authenticate(captor.capture()); + UserIdentity value = captor.getValue(); + assertThat(value.getGroups()).contains("group1", "group2"); + } + + @Test + public void synchronize_groups_on_many_pages() { + mapSettings.setProperty(GITLAB_AUTH_SYNC_USER_GROUPS, "true"); + OAuth2IdentityProvider.CallbackContext callbackContext = Mockito.mock(OAuth2IdentityProvider.CallbackContext.class); + when(callbackContext.getCallbackUrl()).thenReturn("http://server/callback"); + + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + when(httpServletRequest.getParameter("code")).thenReturn(ANY_CODE_VALUE); + when(callbackContext.getRequest()).thenReturn(httpServletRequest); + + gitlab.enqueue(new MockResponse().setBody( + "{\n" + " \"access_token\": \"de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54\",\n" + " \"token_type\": \"bearer\",\n" + " \"expires_in\": 7200,\n" + + " \"refresh_token\": \"8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1\"\n" + "}")); + // response of /user + gitlab.enqueue(new MockResponse().setBody("{\"id\": 123, \"username\": \"username\", \"name\": \"name\"}")); + // response of /groups, first page + gitlab.enqueue(new MockResponse() + .setBody("[{\"full_path\": \"group1\"}, {\"full_path\": \"group2\"}]") + .setHeader("Link", format(" <%s/groups?per_page=100&page=2>; rel=\"next\"," + + " <%s/groups?per_page=100&&page=3>; rel=\"last\"," + + " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl, gitLabUrl))); + // response of /groups, page 2 + gitlab.enqueue(new MockResponse() + .setBody("[{\"full_path\": \"group3\"}, {\"full_path\": \"group4\"}]") + .setHeader("Link", format("<%s/groups?per_page=100&page=3>; rel=\"next\"," + + " <%s/groups?per_page=100&&page=3>; rel=\"last\"," + + " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl, gitLabUrl))); + // response of /groups, page 3 + gitlab.enqueue(new MockResponse() + .setBody("[{\"full_path\": \"group5\"}, {\"full_path\": \"group6\"}]") + .setHeader("Link", format("<%s/groups?per_page=100&&page=3>; rel=\"last\"," + + " <%s/groups?per_page=100&&page=1>; rel=\"first\"", gitLabUrl, gitLabUrl))); + + gitLabIdentityProvider.callback(callbackContext); + + ArgumentCaptor<UserIdentity> captor = ArgumentCaptor.forClass(UserIdentity.class); + verify(callbackContext).authenticate(captor.capture()); + UserIdentity value = captor.getValue(); + assertThat(value.getGroups()).contains("group1", "group2", "group3", "group4", "group5", "group6"); + } + + @Test + public void fail_to_authenticate() { + OAuth2IdentityProvider.CallbackContext callbackContext = Mockito.mock(OAuth2IdentityProvider.CallbackContext.class); + when(callbackContext.getCallbackUrl()).thenReturn("http://server/callback"); + + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + when(httpServletRequest.getParameter("code")).thenReturn(ANY_CODE_VALUE); + when(callbackContext.getRequest()).thenReturn(httpServletRequest); + + gitlab.enqueue(new MockResponse().setBody( + "{\n" + " \"access_token\": \"de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54\",\n" + " \"token_type\": \"bearer\",\n" + " \"expires_in\": 7200,\n" + + " \"refresh_token\": \"8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1\"\n" + "}")); + gitlab.enqueue(new MockResponse().setResponseCode(404).setBody("empty")); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Fail to execute request '" + gitLabSettings.url() + "/api/v4/user'. HTTP code: 404, response: empty"); + + gitLabIdentityProvider.callback(callbackContext); + } + +} diff --git a/server/sonar-web/public/images/gitlab-icon-rgb.svg b/server/sonar-web/public/images/gitlab-icon-rgb.svg new file mode 100644 index 00000000000..e54552cd5fa --- /dev/null +++ b/server/sonar-web/public/images/gitlab-icon-rgb.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" data-name="logo art" width="342.95477391269446" height="318.78894492590746" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/><defs><style>.cls-1{fill:#fc6d26;}.cls-2{fill:#e24329;}.cls-3{fill:#fca326;}</style></defs><title>gitlab-icon-rgb</title><g class="currentLayer" style=""><title>Layer 1</title><g id="g44" class=""><path id="path46" class="cls-1" d="M339.56196038246156,180.72447212219237 l-18.91,-58.12 L283.2319603824615,7.32447212219239 a6.47,6.47 0 0 0 -12.27,0 L233.54196038246153,122.53447212219237 H109.21196038246154 L71.79196038246155,7.32447212219239 a6.46,6.46 0 0 0 -12.26,0 L22.17196038246155,122.53447212219237 l-18.91,58.19 a12.88,12.88 0 0 0 4.66,14.39 L171.39196038246155,313.8944721221924 L334.83196038246155,195.1144721221924 a12.9,12.9 0 0 0 4.73,-14.39 "/></g><g id="g48" class=""><path id="path50" class="cls-2" d="M171.39196038246155,313.8044721221924 h0 l62.16,-191.28 H109.26196038246155 L171.39196038246155,313.8044721221924 z"/></g><g id="g56" class=""><path id="path58" class="cls-1" d="M171.39196038246155,313.8044721221924 L109.21196038246154,122.52447212219238 h-87 L171.39196038246155,313.8044721221924 z"/></g><g id="g64" class=""><path id="path66" class="cls-3" d="M22.141960382461548,122.58447212219238 h0 l-18.91,58.12 a12.88,12.88 0 0 0 4.66,14.39 L171.39196038246155,313.8944721221924 L22.141960382461548,122.58447212219238 z"/></g><g id="g72" class=""><path id="path74" class="cls-2" d="M22.17196038246155,122.58447212219238 h87.11 L71.79196038246155,7.384472122192392 a6.47,6.47 0 0 0 -12.27,0 l-37.35,115.2 z"/></g><g id="g76" class=""><path id="path78" class="cls-1" d="M171.39196038246155,313.8044721221924 l62.16,-191.28 H320.69196038246156 L171.39196038246155,313.8044721221924 z"/></g><g id="g80" class=""><path id="path82" class="cls-3" d="M320.63196038246156,122.58447212219238 h0 l18.91,58.12 a12.85,12.85 0 0 1 -4.66,14.39 L171.39196038246155,313.8044721221924 l149.2,-191.22 z"/></g><g id="g84" class=""><path id="path86" class="cls-2" d="M320.6719603824615,122.58447212219238 h-87.1 l37.42,-115.2 a6.46,6.46 0 0 1 12.26,0 l37.42,115.2 z"/></g></g></svg>
\ No newline at end of file diff --git a/server/sonar-webserver/build.gradle b/server/sonar-webserver/build.gradle index 4fc1039102f..87b08f8c225 100644 --- a/server/sonar-webserver/build.gradle +++ b/server/sonar-webserver/build.gradle @@ -12,6 +12,7 @@ dependencies { compile 'com.google.guava:guava' compile 'org.apache.tomcat.embed:tomcat-embed-core' compile project(':sonar-core') + compile project(':server:sonar-auth-gitlab') compile project(':server:sonar-ce-task-projectanalysis') compile project(':server:sonar-process') compile project(':server:sonar-webserver-core') diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index c00cf3852c9..da9712f4f48 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -28,6 +28,7 @@ import org.sonar.api.resources.ResourceTypes; import org.sonar.api.rules.AnnotationRuleParser; import org.sonar.api.rules.XMLRuleParser; import org.sonar.api.server.rule.RulesDefinitionXmlLoader; +import org.sonar.auth.gitlab.GitLabModule; import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule; import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor; import org.sonar.core.component.DefaultResourceTypes; @@ -349,6 +350,7 @@ public class PlatformLevel4 extends PlatformLevel { // authentication AuthenticationModule.class, AuthenticationWsModule.class, + GitLabModule.class, // users UserSessionFactoryImpl.class, |