Переглянути джерело

SONAR-12471 Embed GitHub authentication

tags/8.0
Julien Lancelot 4 роки тому
джерело
коміт
42f0cff638
37 змінених файлів з 2407 додано та 72 видалено
  1. 21
    0
      server/sonar-auth-common/build.gradle
  2. 99
    0
      server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java
  3. 23
    0
      server/sonar-auth-common/src/main/java/org/sonar/auth/package-info.java
  4. 127
    0
      server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java
  5. 26
    0
      server/sonar-auth-github/build.gradle
  6. 161
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubIdentityProvider.java
  7. 42
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubModule.java
  8. 99
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubRestClient.java
  9. 191
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java
  10. 66
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonEmail.java
  11. 77
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonTeam.java
  12. 76
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonUser.java
  13. 41
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/ScribeGitHubApi.java
  14. 32
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityFactory.java
  15. 54
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityFactoryImpl.java
  16. 51
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityGenerator.java
  17. 23
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/package-info.java
  18. 185
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
  19. 37
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubModuleTest.java
  20. 162
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubSettingsTest.java
  21. 60
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonEmailTest.java
  22. 74
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonTeamTest.java
  23. 56
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonUserTest.java
  24. 471
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/IntegrationTest.java
  25. 120
    0
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/UserIdentityFactoryImplTest.java
  26. 1
    0
      server/sonar-auth-gitlab/build.gradle
  27. 3
    1
      server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java
  28. 3
    68
      server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java
  29. 1
    1
      server/sonar-docs/src/pages/instance-administration/delegated-auth.md
  30. 1
    1
      server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
  31. 12
    0
      server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
  32. 4
    0
      server/sonar-web/public/images/github.svg
  33. 1
    0
      server/sonar-webserver/build.gradle
  34. 2
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  35. 3
    0
      settings.gradle
  36. 0
    1
      sonar-application/build.gradle
  37. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 21
- 0
server/sonar-auth-common/build.gradle Переглянути файл

@@ -0,0 +1,21 @@
description = 'SonarQube :: Authentication :: Common'

configurations {
testCompile.extendsFrom compileOnly
}

dependencies {
// please keep the list ordered

compile 'com.github.scribejava:scribejava-apis'
compile 'com.github.scribejava:scribejava-core'

compileOnly 'com.google.code.findbugs:jsr305'

testCompile 'commons-lang:commons-lang'
testCompile 'com.squareup.okhttp3:mockwebserver'
testCompile 'com.squareup.okhttp3:okhttp'
testCompile 'junit:junit'
testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
}

+ 99
- 0
server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java Переглянути файл

@@ -0,0 +1,99 @@
/*
* 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;

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 OAuthRestClient {

private static final int DEFAULT_PAGE_SIZE = 100;
private static final Pattern NEXT_LINK_PATTERN = Pattern.compile(".*<(.*)>; rel=\"next\"");

private OAuthRestClient() {
// Only static method
}

public 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);
}
}

public static <E> List<E> executePaginatedRequest(String request, OAuth20Service scribe, OAuth2AccessToken accessToken, Function<String, List<E>> function) {
List<E> result = new ArrayList<>();
readPage(result, scribe, accessToken, request + "?per_page=" + DEFAULT_PAGE_SIZE, function);
return result;
}

private static <E> void readPage(List<E> result, OAuth20Service scribe, OAuth2AccessToken accessToken, String endPoint, Function<String, List<E>> function) {
try (Response nextResponse = executeRequest(endPoint, scribe, accessToken)) {
String content = nextResponse.getBody();
if (content == null) {
return;
}
result.addAll(function.apply(content));
readNextEndPoint(nextResponse).ifPresent(newNextEndPoint -> readPage(result, scribe, accessToken, newNextEndPoint, function));
} catch (IOException e) {
throw new IllegalStateException(format("Failed to get %s", endPoint), 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()));
}

}

+ 23
- 0
server/sonar-auth-common/src/main/java/org/sonar/auth/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;

import javax.annotation.ParametersAreNonnullByDefault;

+ 127
- 0
server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.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;

import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.builder.api.DefaultApi20;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.oauth.OAuth20Service;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
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 static java.lang.String.format;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.auth.OAuthRestClient.executePaginatedRequest;
import static org.sonar.auth.OAuthRestClient.executeRequest;

public class OAuthRestClientTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Rule
public MockWebServer mockWebServer = new MockWebServer();

private OAuth2AccessToken auth2AccessToken = mock(OAuth2AccessToken.class);

private String serverUrl;

private OAuth20Service oAuth20Service = new ServiceBuilder("API_KEY")
.apiSecret("API_SECRET")
.callback("CALLBACK")
.build(new TestAPI());

@Before
public void setUp() {
this.serverUrl = format("http://%s:%d", mockWebServer.getHostName(), mockWebServer.getPort());
}

@Test
public void execute_request() throws IOException {
String body = randomAlphanumeric(10);
mockWebServer.enqueue(new MockResponse().setBody(body));

Response response = executeRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken);

assertThat(response.getBody()).isEqualTo(body);
}

@Test
public void fail_to_execute_request() throws IOException {
mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("Error!"));

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage(format("Fail to execute request '%s/test'. HTTP code: 404, response: Error!", serverUrl));

executeRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken);
}

@Test
public void execute_paginated_request() {
mockWebServer.enqueue(new MockResponse()
.setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=2>; rel=\"next\", <" + serverUrl + "/test?per_page=100&page=2>; rel=\"last\"")
.setBody("A"));
mockWebServer.enqueue(new MockResponse()
.setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=1>; rel=\"prev\", <" + serverUrl + "/test?per_page=100&page=1>; rel=\"first\"")
.setBody("B"));

List<String> response = executePaginatedRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken, Arrays::asList);

assertThat(response).contains("A", "B");
}

@Test
public void fail_to_executed_paginated_request() {
mockWebServer.enqueue(new MockResponse()
.setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=2>; rel=\"next\", <" + serverUrl + "/test?per_page=100&page=2>; rel=\"last\"")
.setBody("A"));
mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("Error!"));

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage(format("Fail to execute request '%s/test?per_page=100&page=2'. HTTP code: 404, response: Error!", serverUrl));

executePaginatedRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken, Arrays::asList);
}

private class TestAPI extends DefaultApi20 {

@Override
public String getAccessTokenEndpoint() {
return serverUrl + "/login/oauth/access_token";
}

@Override
protected String getAuthorizationBaseUrl() {
return serverUrl + "/login/oauth/authorize";
}

}
}

+ 26
- 0
server/sonar-auth-github/build.gradle Переглянути файл

@@ -0,0 +1,26 @@
description = 'SonarQube :: Authentication :: GitHub'

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'
compile project(':server:sonar-auth-common')

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'
}

+ 161
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubIdentityProvider.java Переглянути файл

@@ -0,0 +1,161 @@
/*
* 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.github;

import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.oauth.OAuth20Service;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
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.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;

public class GitHubIdentityProvider implements OAuth2IdentityProvider {

static final String KEY = "github";

private final GitHubSettings settings;
private final UserIdentityFactory userIdentityFactory;
private final ScribeGitHubApi scribeApi;
private final GitHubRestClient gitHubRestClient;

public GitHubIdentityProvider(GitHubSettings settings, UserIdentityFactory userIdentityFactory, ScribeGitHubApi scribeApi, GitHubRestClient gitHubRestClient) {
this.settings = settings;
this.userIdentityFactory = userIdentityFactory;
this.scribeApi = scribeApi;
this.gitHubRestClient = gitHubRestClient;
}

@Override
public String getKey() {
return KEY;
}

@Override
public String getName() {
return "GitHub";
}

@Override
public Display getDisplay() {
return Display.builder()
.setIconPath("/images/github.svg")
.setBackgroundColor("#444444")
.build();
}

@Override
public boolean isEnabled() {
return settings.isEnabled();
}

@Override
public boolean allowsUsersToSignUp() {
return settings.allowUsersToSignUp();
}

@Override
public void init(InitContext context) {
String state = context.generateCsrfState();
OAuth20Service scribe = newScribeBuilder(context)
.defaultScope(getScope())
.build(scribeApi);
String url = scribe.getAuthorizationUrl(state);
context.redirectTo(url);
}

String getScope() {
return (settings.syncGroups() || isOrganizationMembershipRequired()) ? "user:email,read:org" : "user:email";
}

@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 {
context.verifyCsrfState();

HttpServletRequest request = context.getRequest();
OAuth20Service scribe = newScribeBuilder(context).build(scribeApi);
String code = request.getParameter("code");
OAuth2AccessToken accessToken = scribe.getAccessToken(code);

GsonUser user = gitHubRestClient.getUser(scribe, accessToken);
check(scribe, accessToken, user);

final String email;
if (user.getEmail() == null) {
// if the user has not specified a public email address in their profile
email = gitHubRestClient.getEmail(scribe, accessToken);
} else {
email = user.getEmail();
}

UserIdentity userIdentity = userIdentityFactory.create(user, email,
settings.syncGroups() ? gitHubRestClient.getTeams(scribe, accessToken) : null);
context.authenticate(userIdentity);
context.redirectToRequestedPage();
}

boolean isOrganizationMembershipRequired() {
return settings.organizations().length > 0;
}

private void check(OAuth20Service scribe, OAuth2AccessToken accessToken, GsonUser user) throws InterruptedException, ExecutionException, IOException {
if (isUnauthorized(scribe, accessToken, user.getLogin())) {
throw new UnauthorizedException(format("'%s' must be a member of at least one organization: '%s'", user.getLogin(), String.join("', '", settings.organizations())));
}
}

private boolean isUnauthorized(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException {
return isOrganizationMembershipRequired() && !isOrganizationsMember(scribe, accessToken, login);
}

private boolean isOrganizationsMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException {
for (String organization : settings.organizations()) {
if (gitHubRestClient.isOrganizationMember(scribe, accessToken, organization, login)) {
return true;
}
}
return false;
}

private ServiceBuilder newScribeBuilder(OAuth2IdentityProvider.OAuth2Context context) {
checkState(isEnabled(), "GitHub authentication is disabled");
return new ServiceBuilder(settings.clientId())
.apiSecret(settings.clientSecret())
.callback(context.getCallbackUrl());
}

}

+ 42
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubModule.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.github;

import java.util.List;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.core.platform.Module;

import static org.sonar.auth.github.GitHubSettings.definitions;

public class GitHubModule extends Module {

@Override
protected void configureModule() {
add(
GitHubIdentityProvider.class,
GitHubSettings.class,
GitHubRestClient.class,
UserIdentityFactoryImpl.class,
ScribeGitHubApi.class);
List<PropertyDefinition> definitions = definitions();
add(definitions.toArray(new Object[definitions.size()]));
}

}

+ 99
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubRestClient.java Переглянути файл

@@ -0,0 +1,99 @@
/*
* 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.github;

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.net.HttpURLConnection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

import static java.lang.String.format;
import static org.sonar.auth.OAuthRestClient.executePaginatedRequest;
import static org.sonar.auth.OAuthRestClient.executeRequest;

public class GitHubRestClient {

private static final Logger LOGGER = Loggers.get(GitHubRestClient.class);

private final GitHubSettings settings;

public GitHubRestClient(GitHubSettings settings) {
this.settings = settings;
}

GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException {
String responseBody = executeRequest(settings.apiURL() + "user", scribe, accessToken).getBody();
LOGGER.trace("User response received : {}", responseBody);
return GsonUser.parse(responseBody);
}

String getEmail(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException {
String responseBody = executeRequest(settings.apiURL() + "user/emails", scribe, accessToken).getBody();
LOGGER.trace("Emails response received : {}", responseBody);
List<GsonEmail> emails = GsonEmail.parse(responseBody);
return emails.stream()
.filter(email -> email.isPrimary() && email.isVerified())
.findFirst()
.map(GsonEmail::getEmail)
.orElse(null);
}

List<GsonTeam> getTeams(OAuth20Service scribe, OAuth2AccessToken accessToken) {
return executePaginatedRequest(settings.apiURL() + "user/teams", scribe, accessToken, GsonTeam::parse);
}

/**
* Check to see that login is a member of organization.
*
* A 204 response code indicates organization membership. 302 and 404 codes are not treated as exceptional,
* they indicate various ways in which a login is not a member of the organization.
*
* @see <a href="https://developer.github.com/v3/orgs/members/#response-if-requester-is-an-organization-member-and-user-is-a-member">GitHub members API</a>
*/
boolean isOrganizationMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String organization, String login)
throws IOException, ExecutionException, InterruptedException {
String requestUrl = settings.apiURL() + format("orgs/%s/members/%s", organization, login);
OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl);
scribe.signRequest(accessToken, request);

Response response = scribe.execute(request);
int code = response.getCode();
switch (code) {
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_NOT_FOUND:
case HttpURLConnection.HTTP_NO_CONTENT:
LOGGER.trace("Orgs response received : {}", code);
return code == HttpURLConnection.HTTP_NO_CONTENT;
default:
throw unexpectedResponseCode(requestUrl, response);
}
}

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()));
}
}

+ 191
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java Переглянути файл

@@ -0,0 +1,191 @@
/*
* 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.github;

import java.util.Arrays;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.config.Configuration;
import org.sonar.api.config.PropertyDefinition;

import static java.lang.String.format;
import static java.lang.String.valueOf;
import static org.sonar.api.PropertyType.BOOLEAN;
import static org.sonar.api.PropertyType.SINGLE_SELECT_LIST;
import static org.sonar.api.PropertyType.STRING;

public class GitHubSettings {

private static final String CLIENT_ID = "sonar.auth.github.clientId.secured";
private static final String CLIENT_SECRET = "sonar.auth.github.clientSecret.secured";
private static final String ENABLED = "sonar.auth.github.enabled";
private static final String ALLOW_USERS_TO_SIGN_UP = "sonar.auth.github.allowUsersToSignUp";
private static final String GROUPS_SYNC = "sonar.auth.github.groupsSync";
private static final String API_URL = "sonar.auth.github.apiUrl";
private static final String WEB_URL = "sonar.auth.github.webUrl";

static final String LOGIN_STRATEGY = "sonar.auth.github.loginStrategy";
static final String LOGIN_STRATEGY_UNIQUE = "Unique";
static final String LOGIN_STRATEGY_PROVIDER_ID = "Same as GitHub login";
static final String LOGIN_STRATEGY_DEFAULT_VALUE = LOGIN_STRATEGY_UNIQUE;

private static final String ORGANIZATIONS = "sonar.auth.github.organizations";

private static final String CATEGORY = "security";
private static final String SUBCATEGORY = "github";

private final Configuration configuration;

public GitHubSettings(Configuration configuration) {
this.configuration = configuration;
}

String clientId() {
return configuration.get(CLIENT_ID).orElse("");
}

String clientSecret() {
return configuration.get(CLIENT_SECRET).orElse("");
}

boolean isEnabled() {
return configuration.getBoolean(ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty();
}

boolean allowUsersToSignUp() {
return configuration.getBoolean(ALLOW_USERS_TO_SIGN_UP).orElse(false);
}

String loginStrategy() {
return configuration.get(LOGIN_STRATEGY).orElse("");
}

boolean syncGroups() {
return configuration.getBoolean(GROUPS_SYNC).orElse(false);
}

@CheckForNull
String webURL() {
return urlWithEndingSlash(configuration.get(WEB_URL).orElse(""));
}

@CheckForNull
String apiURL() {
return urlWithEndingSlash(configuration.get(API_URL).orElse(""));
}

String[] organizations() {
return configuration.getStringArray(ORGANIZATIONS);
}

@CheckForNull
private static String urlWithEndingSlash(@Nullable String url) {
if (url != null && !url.endsWith("/")) {
return url + "/";
}
return url;
}

public static List<PropertyDefinition> definitions() {
return Arrays.asList(
PropertyDefinition.builder(ENABLED)
.name("Enabled")
.description("Enable GitHub users to login. Value is ignored if client ID and secret are not defined.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.type(BOOLEAN)
.defaultValue(valueOf(false))
.index(1)
.build(),
PropertyDefinition.builder(CLIENT_ID)
.name("Client ID")
.description("Client ID provided by GitHub when registering the application.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.index(2)
.build(),
PropertyDefinition.builder(CLIENT_SECRET)
.name("Client Secret")
.description("Client password provided by GitHub when registering the application.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.index(3)
.build(),
PropertyDefinition.builder(ALLOW_USERS_TO_SIGN_UP)
.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(4)
.build(),
PropertyDefinition.builder(LOGIN_STRATEGY)
.name("Login generation strategy")
.description(format("When the login strategy is set to '%s', the user's login will be auto-generated the first time so that it is unique. " +
"When the login strategy is set to '%s', the user's login will be the GitHub login.",
LOGIN_STRATEGY_UNIQUE, LOGIN_STRATEGY_PROVIDER_ID))
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.type(SINGLE_SELECT_LIST)
.defaultValue(LOGIN_STRATEGY_DEFAULT_VALUE)
.options(LOGIN_STRATEGY_UNIQUE, LOGIN_STRATEGY_PROVIDER_ID)
.index(5)
.build(),
PropertyDefinition.builder(GROUPS_SYNC)
.name("Synchronize teams as groups")
.description("For each team he belongs to, the user will be associated to a group named 'Organisation/Team' (if it exists) in SonarQube.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.type(BOOLEAN)
.defaultValue(valueOf(false))
.index(6)
.build(),
PropertyDefinition.builder(API_URL)
.name("The API url for a GitHub instance.")
.description("The API url for a GitHub instance. https://api.github.com/ for github.com, https://github.company.com/api/v3/ when using Github Enterprise")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.type(STRING)
.defaultValue(valueOf("https://api.github.com/"))
.index(7)
.build(),
PropertyDefinition.builder(WEB_URL)
.name("The WEB url for a GitHub instance.")
.description("The WEB url for a GitHub instance. " +
"https://github.com/ for github.com, https://github.company.com/ when using GitHub Enterprise.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.type(STRING)
.defaultValue(valueOf("https://github.com/"))
.index(8)
.build(),
PropertyDefinition.builder(ORGANIZATIONS)
.name("Organizations")
.description("Only members of these organizations will be able to authenticate to the server. " +
"If a user is a member of any of the organizations listed they will be authenticated.")
.multiValues(true)
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.index(9)
.build());
}
}

+ 66
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonEmail.java Переглянути файл

@@ -0,0 +1,66 @@
/*
* 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.github;

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://api.github.com/user/emails
*/
public class GsonEmail {

private String email;
private boolean verified;
private boolean primary;

public GsonEmail() {
// http://stackoverflow.com/a/18645370/229031
this("", false, false);
}

public GsonEmail(String email, boolean verified, boolean primary) {
this.email = email;
this.verified = verified;
this.primary = primary;
}

public String getEmail() {
return email;
}

public boolean isVerified() {
return verified;
}

public boolean isPrimary() {
return primary;
}

public static List<GsonEmail> parse(String json) {
Type collectionType = new TypeToken<Collection<GsonEmail>>() {
}.getType();
Gson gson = new Gson();
return gson.fromJson(json, collectionType);
}
}

+ 77
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonTeam.java Переглянути файл

@@ -0,0 +1,77 @@
/*
* 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.github;

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://api.github.com/user/teams
*/
public class GsonTeam {

private String slug;
private GsonOrganization organization;

public GsonTeam() {
// http://stackoverflow.com/a/18645370/229031
this("", new GsonOrganization());
}

public GsonTeam(String slug, GsonOrganization organization) {
this.slug = slug;
this.organization = organization;
}

public String getId() {
return slug;
}

public String getOrganizationId() {
return organization.getLogin();
}

public static List<GsonTeam> parse(String json) {
Type collectionType = new TypeToken<Collection<GsonTeam>>() {
}.getType();
Gson gson = new Gson();
return gson.fromJson(json, collectionType);
}

public static class GsonOrganization {
private String login;

public GsonOrganization() {
// http://stackoverflow.com/a/18645370/229031
this("");
}

public GsonOrganization(String login) {
this.login = login;
}

public String getLogin() {
return login;
}
}
}

+ 76
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonUser.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.github;

import com.google.gson.Gson;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

/**
* Lite representation of JSON response of GET https://api.github.com/user
*/
public class GsonUser {
private String id;
private String login;
private String name;
private String email;

public GsonUser() {
// even if empty constructor is not required for Gson, it is strongly
// recommended:
// http://stackoverflow.com/a/18645370/229031
}

public GsonUser(String id, String login, @Nullable String name, @Nullable String email) {
this.id = id;
this.login = login;
this.name = name;
this.email = email;
}

public String getId() {
return id;
}

public String getLogin() {
return login;
}

/**
* Name is optional at GitHub
*/
@CheckForNull
public String getName() {
return name;
}

/**
* Name is optional at GitHub
*/
@CheckForNull
public String getEmail() {
return email;
}

public static GsonUser parse(String json) {
Gson gson = new Gson();
return gson.fromJson(json, GsonUser.class);
}
}

+ 41
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/ScribeGitHubApi.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.github;

import com.github.scribejava.apis.GitHubApi;

public class ScribeGitHubApi extends GitHubApi {
private final GitHubSettings settings;

public ScribeGitHubApi(GitHubSettings settings) {
this.settings = settings;
}

@Override
public String getAccessTokenEndpoint() {
return settings.webURL() + "login/oauth/access_token";
}

@Override
protected String getAuthorizationBaseUrl() {
return settings.webURL() + "login/oauth/authorize";
}

}

+ 32
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityFactory.java Переглянути файл

@@ -0,0 +1,32 @@
/*
* 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.github;

import java.util.List;
import javax.annotation.Nullable;
import org.sonar.api.server.authentication.UserIdentity;

/**
* Converts GitHub JSON response to {@link UserIdentity}
*/
public interface UserIdentityFactory {

UserIdentity create(GsonUser user, @Nullable String email, @Nullable List<GsonTeam> teams);
}

+ 54
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityFactoryImpl.java Переглянути файл

@@ -0,0 +1,54 @@
/*
* 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.github;

import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.server.authentication.UserIdentity;

import static org.sonar.auth.github.UserIdentityGenerator.generateLogin;
import static org.sonar.auth.github.UserIdentityGenerator.generateName;

public class UserIdentityFactoryImpl implements UserIdentityFactory {

private final GitHubSettings settings;

public UserIdentityFactoryImpl(GitHubSettings settings) {
this.settings = settings;
}

@Override
public UserIdentity create(GsonUser user, @Nullable String email, @Nullable List<GsonTeam> teams) {
UserIdentity.Builder builder = UserIdentity.builder()
.setProviderId(user.getId())
.setProviderLogin(user.getLogin())
.setLogin(generateLogin(user, settings.loginStrategy()))
.setName(generateName(user))
.setEmail(email);
if (teams != null) {
builder.setGroups(teams.stream()
.map(team -> team.getOrganizationId() + "/" + team.getId())
.collect(Collectors.toSet()));
}
return builder.build();
}

}

+ 51
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/UserIdentityGenerator.java Переглянути файл

@@ -0,0 +1,51 @@
/*
* 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.github;

import static java.lang.String.format;
import static org.sonar.auth.github.GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID;
import static org.sonar.auth.github.GitHubSettings.LOGIN_STRATEGY_UNIQUE;

class UserIdentityGenerator {

private UserIdentityGenerator() {
// Only static method
}

static String generateLogin(GsonUser gsonUser, String loginStrategy) {
switch (loginStrategy) {
case LOGIN_STRATEGY_PROVIDER_ID:
return gsonUser.getLogin();
case LOGIN_STRATEGY_UNIQUE:
return generateUniqueLogin(gsonUser);
default:
throw new IllegalStateException(format("Login strategy not supported : %s", loginStrategy));
}
}

static String generateName(GsonUser gson) {
String name = gson.getName();
return name == null || name.isEmpty() ? gson.getLogin() : name;
}

private static String generateUniqueLogin(GsonUser gsonUser) {
return format("%s@%s", gsonUser.getLogin(), GitHubIdentityProvider.KEY);
}
}

+ 23
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/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.github;

import javax.annotation.ParametersAreNonnullByDefault;

+ 185
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java Переглянути файл

@@ -0,0 +1,185 @@
/*
* 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.github;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.config.internal.MapSettings;
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;
import static org.sonar.auth.github.GitHubSettings.LOGIN_STRATEGY_DEFAULT_VALUE;

public class GitHubIdentityProviderTest {

@Rule
public ExpectedException thrown = ExpectedException.none();

private MapSettings settings = new MapSettings();
private GitHubSettings gitHubSettings = new GitHubSettings(settings.asConfig());
private UserIdentityFactoryImpl userIdentityFactory = mock(UserIdentityFactoryImpl.class);
private ScribeGitHubApi scribeApi = new ScribeGitHubApi(gitHubSettings);
private GitHubRestClient gitHubRestClient = new GitHubRestClient(gitHubSettings);
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient);

@Test
public void check_fields() {
assertThat(underTest.getKey()).isEqualTo("github");
assertThat(underTest.getName()).isEqualTo("GitHub");
assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/github.svg");
assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#444444");
}

@Test
public void is_enabled() {
settings.setProperty("sonar.auth.github.clientId.secured", "id");
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret");
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_DEFAULT_VALUE);
settings.setProperty("sonar.auth.github.enabled", true);
assertThat(underTest.isEnabled()).isTrue();

settings.setProperty("sonar.auth.github.enabled", false);
assertThat(underTest.isEnabled()).isFalse();
}

@Test
public void should_allow_users_to_signup() {
assertThat(underTest.allowsUsersToSignUp()).as("default").isFalse();

settings.setProperty("sonar.auth.github.allowUsersToSignUp", true);
assertThat(underTest.allowsUsersToSignUp()).isTrue();
}

@Test
public void init() {
setSettings(true);
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/");

underTest.init(context);

verify(context).redirectTo("https://github.com/login/oauth/authorize" +
"?response_type=code" +
"&client_id=id" +
"&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&scope=user%3Aemail" +
"&state=state");
}

@Test
public void init_when_group_sync() {
setSettings(true);
settings.setProperty("sonar.auth.github.groupsSync", "true");
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/");
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");

underTest.init(context);

verify(context).redirectTo("https://github.com/login/oauth/authorize" +
"?response_type=code" +
"&client_id=id" +
"&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&scope=user%3Aemail%2Cread%3Aorg" +
"&state=state");
}

@Test
public void init_when_organizations() {
setSettings(true);
settings.setProperty("sonar.auth.github.organizations", "example");
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/");
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");

underTest.init(context);

verify(context).redirectTo("https://github.com/login/oauth/authorize" +
"?response_type=code" +
"&client_id=id" +
"&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback" +
"&scope=user%3Aemail%2Cread%3Aorg" +
"&state=state");
}

@Test
public void fail_to_init_when_disabled() {
setSettings(false);
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);

thrown.expect(IllegalStateException.class);
thrown.expectMessage("GitHub authentication is disabled");
underTest.init(context);
}

@Test
public void scope_includes_org_when_necessary() {
setSettings(false);

settings.setProperty("sonar.auth.github.groupsSync", false);
settings.setProperty("sonar.auth.github.organizations", "");
assertThat(underTest.getScope()).isEqualTo("user:email");

settings.setProperty("sonar.auth.github.groupsSync", true);
settings.setProperty("sonar.auth.github.organizations", "");
assertThat(underTest.getScope()).isEqualTo("user:email,read:org");

settings.setProperty("sonar.auth.github.groupsSync", false);
settings.setProperty("sonar.auth.github.organizations", "example");
assertThat(underTest.getScope()).isEqualTo("user:email,read:org");

settings.setProperty("sonar.auth.github.groupsSync", true);
settings.setProperty("sonar.auth.github.organizations", "example");
assertThat(underTest.getScope()).isEqualTo("user:email,read:org");
}

@Test
public void organization_membership_required() {
setSettings(true);
settings.setProperty("sonar.auth.github.organizations", "example");
assertThat(underTest.isOrganizationMembershipRequired()).isTrue();
settings.setProperty("sonar.auth.github.organizations", "example0, example1");
assertThat(underTest.isOrganizationMembershipRequired()).isTrue();
}

@Test
public void organization_membership_not_required() {
setSettings(true);
settings.setProperty("sonar.auth.github.organizations", "");
assertThat(underTest.isOrganizationMembershipRequired()).isFalse();
}

private void setSettings(boolean enabled) {
if (enabled) {
settings.setProperty("sonar.auth.github.clientId.secured", "id");
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret");
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_DEFAULT_VALUE);
settings.setProperty("sonar.auth.github.enabled", true);
} else {
settings.setProperty("sonar.auth.github.enabled", false);
}
}
}

+ 37
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubModuleTest.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.github;

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 GitHubModuleTest {

@Test
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new GitHubModule().configure(container);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 14);
}

}

+ 162
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubSettingsTest.java Переглянути файл

@@ -0,0 +1,162 @@
/*
* 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.github;

import org.junit.Test;
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.github.GitHubSettings.LOGIN_STRATEGY_DEFAULT_VALUE;
import static org.sonar.auth.github.GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID;

public class GitHubSettingsTest {

private MapSettings settings = new MapSettings(new PropertyDefinitions(GitHubSettings.definitions()));

private GitHubSettings underTest = new GitHubSettings(settings.asConfig());

@Test
public void is_enabled() {
settings.setProperty("sonar.auth.github.clientId.secured", "id");
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret");
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_DEFAULT_VALUE);

settings.setProperty("sonar.auth.github.enabled", true);
assertThat(underTest.isEnabled()).isTrue();

settings.setProperty("sonar.auth.github.enabled", false);
assertThat(underTest.isEnabled()).isFalse();
}

@Test
public void is_enabled_always_return_false_when_client_id_is_null() {
settings.setProperty("sonar.auth.github.enabled", true);
settings.setProperty("sonar.auth.github.clientId.secured", (String) null);
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret");
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_DEFAULT_VALUE);

assertThat(underTest.isEnabled()).isFalse();
}

@Test
public void is_enabled_always_return_false_when_client_secret_is_null() {
settings.setProperty("sonar.auth.github.enabled", true);
settings.setProperty("sonar.auth.github.clientId.secured", "id");
settings.setProperty("sonar.auth.github.clientSecret.secured", (String) null);
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_DEFAULT_VALUE);

assertThat(underTest.isEnabled()).isFalse();
}

@Test
public void return_client_id() {
settings.setProperty("sonar.auth.github.clientId.secured", "id");
assertThat(underTest.clientId()).isEqualTo("id");
}

@Test
public void return_client_secret() {
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret");
assertThat(underTest.clientSecret()).isEqualTo("secret");
}

@Test
public void return_login_strategy() {
settings.setProperty("sonar.auth.github.loginStrategy", LOGIN_STRATEGY_PROVIDER_ID);
assertThat(underTest.loginStrategy()).isEqualTo(LOGIN_STRATEGY_PROVIDER_ID);
}

@Test
public void allow_users_to_sign_up() {
settings.setProperty("sonar.auth.github.allowUsersToSignUp", "true");
assertThat(underTest.allowUsersToSignUp()).isTrue();

settings.setProperty("sonar.auth.github.allowUsersToSignUp", "false");
assertThat(underTest.allowUsersToSignUp()).isFalse();

// default value
settings.setProperty("sonar.auth.github.allowUsersToSignUp", (String) null);
assertThat(underTest.allowUsersToSignUp()).isTrue();
}

@Test
public void sync_groups() {
settings.setProperty("sonar.auth.github.groupsSync", "true");
assertThat(underTest.syncGroups()).isTrue();

settings.setProperty("sonar.auth.github.groupsSync", "false");
assertThat(underTest.syncGroups()).isFalse();

// default value
settings.setProperty("sonar.auth.github.groupsSync", (String) null);
assertThat(underTest.syncGroups()).isFalse();
}

@Test
public void apiUrl_must_have_ending_slash() {
settings.setProperty("sonar.auth.github.apiUrl", "https://github.com");
assertThat(underTest.apiURL()).isEqualTo("https://github.com/");

settings.setProperty("sonar.auth.github.apiUrl", "https://github.com/");
assertThat(underTest.apiURL()).isEqualTo("https://github.com/");
}

@Test
public void webUrl_must_have_ending_slash() {
settings.setProperty("sonar.auth.github.webUrl", "https://github.com");
assertThat(underTest.webURL()).isEqualTo("https://github.com/");

settings.setProperty("sonar.auth.github.webUrl", "https://github.com/");
assertThat(underTest.webURL()).isEqualTo("https://github.com/");
}

@Test
public void return_organizations_single() {
String setting = "example";
settings.setProperty("sonar.auth.github.organizations", setting);
String[] expected = new String[] {"example"};
String[] actual = underTest.organizations();
assertThat(actual).isEqualTo(expected);
}

@Test
public void return_organizations_multiple() {
String setting = "example0,example1";
settings.setProperty("sonar.auth.github.organizations", setting);
String[] expected = new String[] {"example0", "example1"};
String[] actual = underTest.organizations();
assertThat(actual).isEqualTo(expected);
}

@Test
public void return_organizations_empty_list() {
String[] setting = null;
settings.setProperty("sonar.auth.github.organizations", setting);
String[] expected = new String[] {};
String[] actual = underTest.organizations();
assertThat(actual).isEqualTo(expected);
}

@Test
public void definitions() {
assertThat(GitHubSettings.definitions()).hasSize(9);
}
}

+ 60
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonEmailTest.java Переглянути файл

@@ -0,0 +1,60 @@
/*
* 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.github;

import java.util.List;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class GsonEmailTest {

@Test
public void parse() {
List<GsonEmail> underTest = GsonEmail.parse(
"[\n" +
" {\n" +
" \"email\": \"octocat@github.com\",\n" +
" \"verified\": true,\n" +
" \"primary\": true\n" +
" },\n" +
" {\n" +
" \"email\": \"support@github.com\",\n" +
" \"verified\": false,\n" +
" \"primary\": false\n" +
" }\n" +
"]");
assertThat(underTest).hasSize(2);

assertThat(underTest.get(0).getEmail()).isEqualTo("octocat@github.com");
assertThat(underTest.get(0).isVerified()).isTrue();
assertThat(underTest.get(0).isPrimary()).isTrue();

assertThat(underTest.get(1).getEmail()).isEqualTo("support@github.com");
assertThat(underTest.get(1).isVerified()).isFalse();
assertThat(underTest.get(1).isPrimary()).isFalse();
}

@Test
public void should_have_no_arg_constructor() {
assertThat(new GsonEmail().getEmail()).isEqualTo("");
}

}

+ 74
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonTeamTest.java Переглянути файл

@@ -0,0 +1,74 @@
/*
* 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.github;

import java.util.List;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class GsonTeamTest {

@Test
public void parse_one_team() {
List<GsonTeam> underTest = GsonTeam.parse(
"[\n" +
" {\n" +
" \"name\": \"Developers\",\n" +
" \"slug\": \"developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarSource\"\n" +
" }\n" +
" }\n" +
"]");
assertThat(underTest).hasSize(1);

assertThat(underTest.get(0).getId()).isEqualTo("developers");
assertThat(underTest.get(0).getOrganizationId()).isEqualTo("SonarSource");
}

@Test
public void parse_two_teams() {
List<GsonTeam> underTest = GsonTeam.parse(
"[\n" +
" {\n" +
" \"name\": \"Developers\",\n" +
" \"slug\": \"developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarSource\"\n" +
" }\n" +
" },\n" +
" {\n" +
" \"login\": \"SonarSource Developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarQubeCommunity\"\n" +
" }\n" +
" }\n" +
"]");
assertThat(underTest).hasSize(2);
}

@Test
public void should_have_no_arg_constructor() {
assertThat(new GsonTeam().getId()).isEqualTo("");
assertThat(new GsonTeam.GsonOrganization().getLogin()).isEqualTo("");
}

}

+ 56
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GsonUserTest.java Переглянути файл

@@ -0,0 +1,56 @@
/*
* 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.github;

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class GsonUserTest {

@Test
public void parse_json() {
GsonUser user = GsonUser.parse(
"{\n" +
" \"login\": \"octocat\",\n" +
" \"id\": 1,\n" +
" \"name\": \"monalisa octocat\",\n" +
" \"email\": \"octocat@github.com\"\n" +
"}");
assertThat(user.getId()).isEqualTo("1");
assertThat(user.getLogin()).isEqualTo("octocat");
assertThat(user.getName()).isEqualTo("monalisa octocat");
assertThat(user.getEmail()).isEqualTo("octocat@github.com");
}

@Test
public void name_can_be_null() {
GsonUser underTest = GsonUser.parse("{login:octocat, email:octocat@github.com}");
assertThat(underTest.getLogin()).isEqualTo("octocat");
assertThat(underTest.getName()).isNull();
}

@Test
public void email_can_be_null() {
GsonUser underTest = GsonUser.parse("{login:octocat}");
assertThat(underTest.getLogin()).isEqualTo("octocat");
assertThat(underTest.getEmail()).isNull();
}
}

+ 471
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/IntegrationTest.java Переглянути файл

@@ -0,0 +1,471 @@
/*
* 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.github;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.PropertyDefinitions;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
import org.sonar.api.server.authentication.UnauthorizedException;
import org.sonar.api.server.authentication.UserIdentity;

import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class IntegrationTest {

private static final String CALLBACK_URL = "http://localhost/oauth/callback/github";

@Rule
public MockWebServer github = new MockWebServer();

// load settings with default values
private MapSettings settings = new MapSettings(new PropertyDefinitions(GitHubSettings.definitions()));
private GitHubSettings gitHubSettings = new GitHubSettings(settings.asConfig());
private UserIdentityFactoryImpl userIdentityFactory = new UserIdentityFactoryImpl(gitHubSettings);
private ScribeGitHubApi scribeApi = new ScribeGitHubApi(gitHubSettings);
private GitHubRestClient gitHubRestClient = new GitHubRestClient(gitHubSettings);

private String gitHubUrl;

private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient);

@Before
public void enable() {
gitHubUrl = format("http://%s:%d", github.getHostName(), github.getPort());
settings.setProperty("sonar.auth.github.clientId.secured", "the_id");
settings.setProperty("sonar.auth.github.clientSecret.secured", "the_secret");
settings.setProperty("sonar.auth.github.enabled", true);
settings.setProperty("sonar.auth.github.apiUrl", gitHubUrl);
settings.setProperty("sonar.auth.github.webUrl", gitHubUrl);
}

/**
* First phase: SonarQube redirects browser to GitHub authentication form, requesting the
* minimal access rights ("scope") to get user profile (login, name, email and others).
*/
@Test
public void redirect_browser_to_github_authentication_form() throws Exception {
DumbInitContext context = new DumbInitContext("the-csrf-state");
underTest.init(context);

assertThat(context.redirectedTo).isEqualTo(
gitHubSettings.webURL() +
"login/oauth/authorize" +
"?response_type=code" +
"&client_id=the_id" +
"&redirect_uri=" + URLEncoder.encode(CALLBACK_URL, StandardCharsets.UTF_8.name()) +
"&scope=" + URLEncoder.encode("user:email", StandardCharsets.UTF_8.name()) +
"&state=the-csrf-state");
}

/**
* Second phase: GitHub redirects browser to SonarQube at /oauth/callback/github?code={the verifier code}.
* This SonarQube web service sends two requests to GitHub:
* <ul>
* <li>get an access token</li>
* <li>get the profile of the authenticated user</li>
* </ul>
*/
@Test
public void callback_on_successful_authentication() throws IOException, InterruptedException {
github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
assertThat(callbackContext.userIdentity.getProviderId()).isEqualTo("ABCD");
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("octocat@github");
assertThat(callbackContext.userIdentity.getName()).isEqualTo("monalisa octocat");
assertThat(callbackContext.userIdentity.getEmail()).isEqualTo("octocat@github.com");
assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue();

// Verify the requests sent to GitHub
RecordedRequest accessTokenGitHubRequest = github.takeRequest();
assertThat(accessTokenGitHubRequest.getMethod()).isEqualTo("POST");
assertThat(accessTokenGitHubRequest.getPath()).isEqualTo("/login/oauth/access_token");
assertThat(accessTokenGitHubRequest.getBody().readUtf8()).isEqualTo(
"code=the-verifier-code" +
"&redirect_uri=" + URLEncoder.encode(CALLBACK_URL, StandardCharsets.UTF_8.name()) +
"&grant_type=authorization_code");

RecordedRequest profileGitHubRequest = github.takeRequest();
assertThat(profileGitHubRequest.getMethod()).isEqualTo("GET");
assertThat(profileGitHubRequest.getPath()).isEqualTo("/user");
}

@Test
public void should_retrieve_private_primary_verified_email_address() {
github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":null}"));
// response of api.github.com/user/emails
github.enqueue(new MockResponse().setBody(
"[\n" +
" {\n" +
" \"email\": \"support@github.com\",\n" +
" \"verified\": false,\n" +
" \"primary\": false\n" +
" },\n" +
" {\n" +
" \"email\": \"octocat@github.com\",\n" +
" \"verified\": true,\n" +
" \"primary\": true\n" +
" },\n" +
"]"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
assertThat(callbackContext.userIdentity.getProviderId()).isEqualTo("ABCD");
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("octocat@github");
assertThat(callbackContext.userIdentity.getName()).isEqualTo("monalisa octocat");
assertThat(callbackContext.userIdentity.getEmail()).isEqualTo("octocat@github.com");
assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue();
}

@Test
public void should_not_fail_if_no_email() {
github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":null}"));
// response of api.github.com/user/emails
github.enqueue(new MockResponse().setBody("[]"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
assertThat(callbackContext.userIdentity.getProviderId()).isEqualTo("ABCD");
assertThat(callbackContext.userIdentity.getLogin()).isEqualTo("octocat@github");
assertThat(callbackContext.userIdentity.getName()).isEqualTo("monalisa octocat");
assertThat(callbackContext.userIdentity.getEmail()).isNull();
assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue();
}

@Test
public void redirect_browser_to_github_authentication_form_with_group_sync() throws Exception {
settings.setProperty("sonar.auth.github.groupsSync", true);
DumbInitContext context = new DumbInitContext("the-csrf-state");
underTest.init(context);
assertThat(context.redirectedTo).isEqualTo(
gitHubSettings.webURL() +
"login/oauth/authorize" +
"?response_type=code" +
"&client_id=the_id" +
"&redirect_uri=" + URLEncoder.encode(CALLBACK_URL, StandardCharsets.UTF_8.name()) +
"&scope=" + URLEncoder.encode("user:email,read:org", StandardCharsets.UTF_8.name()) +
"&state=the-csrf-state");
}

@Test
public void callback_on_successful_authentication_with_group_sync() {
settings.setProperty("sonar.auth.github.groupsSync", true);

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// response of api.github.com/user/teams
github.enqueue(new MockResponse().setBody("[\n" +
" {\n" +
" \"slug\": \"developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarSource\"\n" +
" }\n" +
" }\n" +
"]"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(callbackContext.userIdentity.getGroups()).containsOnly("SonarSource/developers");
}

@Test
public void callback_on_successful_authentication_with_group_sync_on_many_pages() {
settings.setProperty("sonar.auth.github.groupsSync", true);

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// responses of api.github.com/user/teams
github.enqueue(new MockResponse()
.setHeader("Link", "<" + gitHubUrl + "/user/teams?per_page=100&page=2>; rel=\"next\", <" + gitHubUrl + "/user/teams?per_page=100&page=2>; rel=\"last\"")
.setBody("[\n" +
" {\n" +
" \"slug\": \"developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarSource\"\n" +
" }\n" +
" }\n" +
"]"));
github.enqueue(new MockResponse()
.setHeader("Link", "<" + gitHubUrl + "/user/teams?per_page=100&page=1>; rel=\"prev\", <" + gitHubUrl + "/user/teams?per_page=100&page=1>; rel=\"first\"")
.setBody("[\n" +
" {\n" +
" \"slug\": \"sonarsource-developers\",\n" +
" \"organization\": {\n" +
" \"login\": \"SonarQubeCommunity\"\n" +
" }\n" +
" }\n" +
"]"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(new TreeSet<>(callbackContext.userIdentity.getGroups())).containsOnly("SonarQubeCommunity/sonarsource-developers", "SonarSource/developers");
}

@Test
public void redirect_browser_to_github_authentication_form_with_organizations() throws Exception {
settings.setProperty("sonar.auth.github.organizations", "example0, example1");
DumbInitContext context = new DumbInitContext("the-csrf-state");
underTest.init(context);
assertThat(context.redirectedTo).isEqualTo(
gitHubSettings.webURL() +
"login/oauth/authorize" +
"?response_type=code" +
"&client_id=the_id" +
"&redirect_uri=" + URLEncoder.encode(CALLBACK_URL, StandardCharsets.UTF_8.name()) +
"&scope=" + URLEncoder.encode("user:email,read:org", StandardCharsets.UTF_8.name()) +
"&state=the-csrf-state");
}

@Test
public void callback_on_successful_authentication_with_organizations_with_membership() {
settings.setProperty("sonar.auth.github.organizations", "example0, example1");

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// response of api.github.com/orgs/example0/members/user
github.enqueue(new MockResponse().setResponseCode(204));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
assertThat(callbackContext.userIdentity).isNotNull();
assertThat(callbackContext.redirectedToRequestedPage.get()).isTrue();
}

@Test
public void callback_on_successful_authentication_with_organizations_without_membership() {
settings.setProperty("sonar.auth.github.organizations", "first_org,second_org");
settings.setProperty("sonar.auth.github.loginStrategy", GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID);

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// response of api.github.com/orgs/first_org/members/user
github.enqueue(new MockResponse().setResponseCode(404).setBody("{}"));
// response of api.github.com/orgs/second_org/members/user
github.enqueue(new MockResponse().setResponseCode(404).setBody("{}"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
try {
underTest.callback(callbackContext);
fail("exception expected");
} catch (UnauthorizedException e) {
assertThat(e.getMessage()).isEqualTo("'octocat' must be a member of at least one organization: 'first_org', 'second_org'");
}
}

@Test
public void callback_on_successful_authentication_with_organizations_without_membership_with_unique_login_strategy() {
settings.setProperty("sonar.auth.github.organizations", "example");
settings.setProperty("sonar.auth.github.loginStrategy", GitHubSettings.LOGIN_STRATEGY_UNIQUE);

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// response of api.github.com/orgs/example0/members/user
github.enqueue(new MockResponse().setResponseCode(404).setBody("{}"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
try {
underTest.callback(callbackContext);
fail("exception expected");
} catch (UnauthorizedException e) {
assertThat(e.getMessage()).isEqualTo("'octocat' must be a member of at least one organization: 'example'");
}
}

@Test
public void callback_throws_ISE_if_error_when_requesting_user_profile() {
github.enqueue(newSuccessfulAccessTokenResponse());
// api.github.com/user crashes
github.enqueue(new MockResponse().setResponseCode(500).setBody("{error}"));

DumbCallbackContext callbackContext = new DumbCallbackContext(newRequest("the-verifier-code"));
try {
underTest.callback(callbackContext);
fail("exception expected");
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("Fail to execute request '" + gitHubSettings.apiURL() + "user'. HTTP code: 500, response: {error}");
}

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
assertThat(callbackContext.userIdentity).isNull();
assertThat(callbackContext.redirectedToRequestedPage.get()).isFalse();
}

@Test
public void callback_throws_ISE_if_error_when_checking_membership() {
settings.setProperty("sonar.auth.github.organizations", "example");

github.enqueue(newSuccessfulAccessTokenResponse());
// response of api.github.com/user
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}"));
// crash of api.github.com/orgs/example/members/user
github.enqueue(new MockResponse().setResponseCode(500).setBody("{error}"));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
try {
underTest.callback(callbackContext);
fail("exception expected");
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("Fail to execute request '" + gitHubSettings.apiURL() + "orgs/example/members/octocat'. HTTP code: 500, response: {error}");
}
}

/**
* Response sent by GitHub to SonarQube when generating an access token
*/
private static MockResponse newSuccessfulAccessTokenResponse() {
// github does not return the standard JSON format but plain-text
// see https://developer.github.com/v3/oauth/
return new MockResponse().setBody("access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer");
}

private static HttpServletRequest newRequest(String verifierCode) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getParameter("code")).thenReturn(verifierCode);
return request;
}

private static class DumbCallbackContext implements OAuth2IdentityProvider.CallbackContext {
final HttpServletRequest request;
final AtomicBoolean csrfStateVerified = new AtomicBoolean(false);
final AtomicBoolean redirectedToRequestedPage = new AtomicBoolean(false);
UserIdentity userIdentity = null;

public DumbCallbackContext(HttpServletRequest request) {
this.request = request;
}

@Override
public void verifyCsrfState() {
this.csrfStateVerified.set(true);
}

@Override
public void verifyCsrfState(String parameterName) {
throw new UnsupportedOperationException("not used");
}

@Override
public void redirectToRequestedPage() {
redirectedToRequestedPage.set(true);
}

@Override
public void authenticate(UserIdentity userIdentity) {
this.userIdentity = userIdentity;
}

@Override
public String getCallbackUrl() {
return CALLBACK_URL;
}

@Override
public HttpServletRequest getRequest() {
return request;
}

@Override
public HttpServletResponse getResponse() {
throw new UnsupportedOperationException("not used");
}
}

private static class DumbInitContext implements OAuth2IdentityProvider.InitContext {
String redirectedTo = null;
private final String generatedCsrfState;

public DumbInitContext(String generatedCsrfState) {
this.generatedCsrfState = generatedCsrfState;
}

@Override
public String generateCsrfState() {
return generatedCsrfState;
}

@Override
public void redirectTo(String url) {
this.redirectedTo = url;
}

@Override
public String getCallbackUrl() {
return CALLBACK_URL;
}

@Override
public HttpServletRequest getRequest() {
return null;
}

@Override
public HttpServletResponse getResponse() {
return null;
}
}
}

+ 120
- 0
server/sonar-auth-github/src/test/java/org/sonar/auth/github/UserIdentityFactoryImplTest.java Переглянути файл

@@ -0,0 +1,120 @@
/*
* 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.github;

import java.util.Arrays;
import java.util.List;
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 org.sonar.api.server.authentication.UserIdentity;

import static org.assertj.core.api.Assertions.assertThat;

public class UserIdentityFactoryImplTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

private MapSettings settings = new MapSettings(new PropertyDefinitions(GitHubSettings.definitions()));
private UserIdentityFactoryImpl underTest = new UserIdentityFactoryImpl(new GitHubSettings(settings.asConfig()));

/**
* Keep the same login as at GitHub
*/
@Test
public void create_for_provider_strategy() {
GsonUser gson = new GsonUser("ABCD", "octocat", "monalisa octocat", "octocat@github.com");
settings.setProperty(GitHubSettings.LOGIN_STRATEGY, GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID);

UserIdentity identity = underTest.create(gson, gson.getEmail(), null);

assertThat(identity.getProviderId()).isEqualTo("ABCD");
assertThat(identity.getLogin()).isEqualTo("octocat");
assertThat(identity.getName()).isEqualTo("monalisa octocat");
assertThat(identity.getEmail()).isEqualTo("octocat@github.com");
}

@Test
public void no_email() {
GsonUser gson = new GsonUser("ABCD", "octocat", "monalisa octocat", null);
settings.setProperty(GitHubSettings.LOGIN_STRATEGY, GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID);

UserIdentity identity = underTest.create(gson, null, null);

assertThat(identity.getLogin()).isEqualTo("octocat");
assertThat(identity.getName()).isEqualTo("monalisa octocat");
assertThat(identity.getEmail()).isNull();
}

@Test
public void create_for_provider_strategy_with_teams() {
GsonUser gson = new GsonUser("ABCD", "octocat", "monalisa octocat", "octocat@github.com");
List<GsonTeam> teams = Arrays.asList(
new GsonTeam("developers", new GsonTeam.GsonOrganization("SonarSource")));
settings.setProperty(GitHubSettings.LOGIN_STRATEGY, GitHubSettings.LOGIN_STRATEGY_PROVIDER_ID);

UserIdentity identity = underTest.create(gson, null, teams);

assertThat(identity.getGroups()).containsOnly("SonarSource/developers");
}

@Test
public void create_for_unique_login_strategy() {
GsonUser gson = new GsonUser("ABCD", "octocat", "monalisa octocat", "octocat@github.com");
settings.setProperty(GitHubSettings.LOGIN_STRATEGY, GitHubSettings.LOGIN_STRATEGY_UNIQUE);

UserIdentity identity = underTest.create(gson, null, null);

assertThat(identity.getLogin()).isEqualTo("octocat@github");
assertThat(identity.getName()).isEqualTo("monalisa octocat");
assertThat(identity.getEmail()).isNull();
}

@Test
public void empty_name_is_replaced_by_provider_login() {
GsonUser gson = new GsonUser("ABCD", "octocat", "", "octocat@github.com");

UserIdentity identity = underTest.create(gson, null, null);

assertThat(identity.getName()).isEqualTo("octocat");
}

@Test
public void null_name_is_replaced_by_provider_login() {
GsonUser gson = new GsonUser("ABCD", "octocat", null, "octocat@github.com");

UserIdentity identity = underTest.create(gson, null, null);

assertThat(identity.getName()).isEqualTo("octocat");
}

@Test
public void throw_ISE_if_strategy_is_not_supported() {
settings.setProperty(GitHubSettings.LOGIN_STRATEGY, "xxx");

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Login strategy not supported : xxx");

underTest.create(new GsonUser("ABCD", "octocat", "octocat", "octocat@github.com"), null, null);
}
}

+ 1
- 0
server/sonar-auth-gitlab/build.gradle Переглянути файл

@@ -10,6 +10,7 @@ dependencies {
compile 'com.github.scribejava:scribejava-apis'
compile 'com.github.scribejava:scribejava-core'
compile 'com.google.code.gson:gson'
compile project(':server:sonar-auth-common')

compileOnly 'com.google.code.findbugs:jsr305'
compileOnly 'com.squareup.okhttp3:okhttp'

+ 3
- 1
server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabIdentityProvider.java Переглянути файл

@@ -62,7 +62,9 @@ public class GitLabIdentityProvider implements OAuth2IdentityProvider {
@Override
public Display getDisplay() {
return Display.builder()
.setIconPath("/images/gitlab-icon-rgb.svg").setBackgroundColor("#6a4fbb").build();
.setIconPath("/images/gitlab-icon-rgb.svg")
.setBackgroundColor("#6a4fbb")
.build();
}

@Override

+ 3
- 68
server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GitLabRestClient.java Переглянути файл

@@ -20,26 +20,14 @@
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;
import org.sonar.auth.OAuthRestClient;

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;
@@ -49,7 +37,7 @@ public class GitLabRestClient {
}

GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) {
try (Response response = executeRequest(settings.url() + API_SUFFIX + "/user", scribe, accessToken)) {
try (Response response = OAuthRestClient.executeRequest(settings.url() + API_SUFFIX + "/user", scribe, accessToken)) {
String responseBody = response.getBody();
return GsonUser.parse(responseBody);
} catch (IOException e) {
@@ -58,59 +46,6 @@ public class GitLabRestClient {
}

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);
}
return OAuthRestClient.executePaginatedRequest(settings.url() + API_SUFFIX + "/groups", scribe, accessToken, GsonGroup::parse);
}

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()));
}

}

+ 1
- 1
server/sonar-docs/src/pages/instance-administration/delegated-auth.md Переглянути файл

@@ -40,7 +40,7 @@ You can delegate authentication to GitHub Enterprise using a dedicated GitHub OA
1. You'll need to first create a GitHub OAuth application. Click [here](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) for general instructions:
1. "Homepage URL" is the public URL to your SonarQube server, for example "https://sonarqube.mycompany.com". For security reasons HTTP is not supported. HTTPS must be used. The public URL is configured in SonarQube at **[Administration -> General -> Server base URL](/#sonarqube-admin#/admin/settings)**
1. "Authorization callback URL" is <Homepage URL>/oauth2/callback, for example "https://sonarqube.mycompany.com/oauth2/callback"
1. In SonarQube, navigate to **[Administration > Configuration > General Settings > GitHub](/#sonarqube-admin#/admin/settings?category=github)**:
1. In SonarQube, navigate to **[Administration > Configuration > General Settings > Security > GitHub](/#sonarqube-admin#/admin/settings?category=security)**:
1. Set **Enabled** to `true`
1. Set the **Client ID** to the value provided by the GitHub developer application
1. Set the **Client Secret** to the value provided by the GitHub developer application

+ 1
- 1
server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java Переглянути файл

@@ -47,7 +47,7 @@ import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvi
*/
public abstract class ServerExtensionInstaller {

private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgitlab");
private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgithub", "authgitlab");

private final SonarRuntime sonarRuntime;
private final PluginRepository pluginRepository;

+ 12
- 0
server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java Переглянути файл

@@ -64,6 +64,18 @@ public class ServerExtensionInstallerTest {
assertThat(componentContainer.getPicoContainer().getComponents()).contains(fooPlugin);
}

@Test
public void fail_when_detecting_github_auth_plugin() {
PluginInfo foo = newPlugin("authgithub", "GitHub Auth");
pluginRepository.add(foo, mock(Plugin.class));
ComponentContainer componentContainer = new ComponentContainer();

expectedException.expect(MessageException.class);
expectedException.expectMessage("Plugins 'GitHub Auth' are no more compatible with SonarQube");

underTest.installExtensions(componentContainer);
}

@Test
public void fail_when_detecting_gitlab_auth_plugin() {
PluginInfo foo = newPlugin("authgitlab", "GitLab Auth");

+ 4
- 0
server/sonar-web/public/images/github.svg Переглянути файл

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 438.5 438.5" xml:space="preserve">
<path fill="#fff"
d="M409.1 114.6c-19.6-33.6-46.2-60.2-79.8-79.8C295.7 15.2 259.1 5.4 219.3 5.4c-39.8 0-76.5 9.8-110.1 29.4 -33.6 19.6-60.2 46.2-79.8 79.8C9.8 148.2 0 184.9 0 224.6c0 47.8 13.9 90.7 41.8 128.9 27.9 38.2 63.9 64.6 108.1 79.2 5.1 1 8.9 0.3 11.4-2 2.5-2.3 3.7-5.1 3.7-8.6 0-0.6 0-5.7-0.1-15.4 -0.1-9.7-0.1-18.2-0.1-25.4l-6.6 1.1c-4.2 0.8-9.5 1.1-15.8 1 -6.4-0.1-13-0.8-19.8-2 -6.9-1.2-13.2-4.1-19.1-8.6 -5.9-4.5-10.1-10.3-12.6-17.6l-2.9-6.6c-1.9-4.4-4.9-9.2-9-14.6 -4.1-5.3-8.2-8.9-12.4-10.8l-2-1.4c-1.3-1-2.6-2.1-3.7-3.4 -1.1-1.3-2-2.7-2.6-4 -0.6-1.3-0.1-2.4 1.4-3.3 1.5-0.9 4.3-1.3 8.3-1.3l5.7 0.9c3.8 0.8 8.5 3 14.1 6.9 5.6 3.8 10.2 8.8 13.8 14.8 4.4 7.8 9.7 13.8 15.8 17.8 6.2 4.1 12.4 6.1 18.7 6.1 6.3 0 11.7-0.5 16.3-1.4 4.6-1 8.8-2.4 12.8-4.3 1.7-12.8 6.4-22.6 14-29.4 -10.8-1.1-20.6-2.9-29.3-5.1 -8.7-2.3-17.6-6-26.8-11.1 -9.2-5.1-16.9-11.5-23-19.1 -6.1-7.6-11.1-17.6-15-30 -3.9-12.4-5.9-26.6-5.9-42.8 0-23 7.5-42.6 22.6-58.8 -7-17.3-6.4-36.7 2-58.2 5.5-1.7 13.7-0.4 24.6 3.9 10.9 4.3 18.8 8 23.8 11 5 3 9.1 5.6 12.1 7.7 17.7-4.9 36-7.4 54.8-7.4s37.1 2.5 54.8 7.4l10.8-6.8c7.4-4.6 16.2-8.8 26.3-12.6 10.1-3.8 17.8-4.9 23.1-3.1 8.6 21.5 9.3 40.9 2.3 58.2 15 16.2 22.6 35.8 22.6 58.8 0 16.2-2 30.5-5.9 43 -3.9 12.5-8.9 22.5-15.1 30 -6.2 7.5-13.9 13.9-23.1 19 -9.2 5.1-18.2 8.9-26.8 11.1 -8.7 2.3-18.4 4-29.3 5.1 9.9 8.6 14.8 22.1 14.8 40.5v60.2c0 3.4 1.2 6.3 3.6 8.6 2.4 2.3 6.1 3 11.3 2 44.2-14.7 80.2-41.1 108.1-79.2 27.9-38.2 41.8-81.1 41.8-128.9C438.5 184.9 428.7 148.2 409.1 114.6z"/>
</svg>

+ 1
- 0
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-github')
compile project(':server:sonar-auth-gitlab')
compile project(':server:sonar-ce-task-projectanalysis')
compile project(':server:sonar-process')

+ 2
- 0
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.github.GitHubModule;
import org.sonar.auth.gitlab.GitLabModule;
import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule;
import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor;
@@ -351,6 +352,7 @@ public class PlatformLevel4 extends PlatformLevel {
// authentication
AuthenticationModule.class,
AuthenticationWsModule.class,
GitHubModule.class,
GitLabModule.class,

// users

+ 3
- 0
settings.gradle Переглянути файл

@@ -2,6 +2,8 @@ rootProject.name = 'sonarqube'

include 'plugins:sonar-xoo-plugin'

include 'server:sonar-auth-common'
include 'server:sonar-auth-github'
include 'server:sonar-auth-gitlab'
include 'server:sonar-ce'
include 'server:sonar-ce-common'
@@ -51,3 +53,4 @@ buildCache {
enabled = !isCiServer
}
}


+ 0
- 1
sonar-application/build.gradle Переглянути файл

@@ -50,7 +50,6 @@ dependencies {
jdbc_mssql 'com.microsoft.sqlserver:mssql-jdbc'
jdbc_postgresql 'org.postgresql:postgresql'

bundledPlugin 'org.sonarsource.auth.github:sonar-auth-github-plugin:1.5.0.870@jar'
bundledPlugin 'org.sonarsource.auth.saml:sonar-auth-saml-plugin:1.1.0.181@jar'
bundledPlugin 'org.sonarsource.css:sonar-css-plugin@jar'
bundledPlugin "org.sonarsource.dotnet:sonar-csharp-plugin@jar"

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -934,6 +934,8 @@ property.category.general.subProjects=Sub-projects
property.category.organizations=Organizations
property.category.security=Security
property.category.security.encryption=Encryption
property.category.security.github=GitHub
property.category.security.github.description=In order to enable GitHub authentication:<ul><li>SonarQube must be publicly accessible through HTTPS only</li><li>The property 'sonar.core.serverBaseURL' must be set to this public HTTPS URL</li><li>In your GitHub profile, you need to create a Developer Application for which the 'Authorization callback URL' must be set to <code>'&lt;value_of_sonar.core.serverBaseURL_property&gt;/oauth2/callback'</code>.</li></ul>
property.category.security.gitlab=Gitlab
property.category.java=Java
property.category.differentialViews=New Code

Завантаження…
Відмінити
Зберегти