aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2019-09-17 12:21:53 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-23 20:21:07 +0200
commit056302d0d534a30a8a7d905602455b0ba0f900a2 (patch)
tree1e755215fb861b215e97e724239c22036cccc639 /server
parent26a7d44c9cce85b62e6d3f2043aa3eddc500a54e (diff)
downloadsonarqube-056302d0d534a30a8a7d905602455b0ba0f900a2.tar.gz
sonarqube-056302d0d534a30a8a7d905602455b0ba0f900a2.zip
SONAR-12460 Support GitLab Authentication
Diffstat (limited to 'server')
-rw-r--r--server/sonar-auth-gitlab/build.gradle25
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java135
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabModule.java41
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java116
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabSettings.java127
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java55
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonUser.java53
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/ScribeGitLabOauth2Api.java42
-rw-r--r--server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/package-info.java23
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabIdentityProviderTest.java93
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabModuleTest.java37
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GitLabSettingsTest.java76
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java52
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonUserTest.java75
-rw-r--r--server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/IntegrationTest.java192
-rw-r--r--server/sonar-web/public/images/gitlab-icon-rgb.svg1
-rw-r--r--server/sonar-webserver/build.gradle1
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java2
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,