Browse Source

SONAR-21088 Fix SSF-434

tags/10.4.0.87286
Wojtek Wajerowicz 4 months ago
parent
commit
e9fa96f54d
50 changed files with 449 additions and 147 deletions
  1. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/ApplicationHttpClient.java
  2. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericApplicationHttpClient.java
  3. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericPaginatedHttpClient.java
  4. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/PaginatedHttpClient.java
  5. 20
    17
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
  6. 2
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java
  7. 5
    3
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java
  8. 1
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/security/AppToken.java
  9. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabToken.java
  10. 1
    1
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/GenericPaginatedHttpClientImplTest.java
  11. 2
    2
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GenericApplicationHttpClientTest.java
  12. 9
    6
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
  13. 1
    1
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubGlobalSettingsValidatorTest.java
  14. 5
    3
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java
  15. 1
    1
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/security/GithubAppSecurityImplTest.java
  16. 1
    0
      server/sonar-auth-github/build.gradle
  17. 2
    2
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/AppInstallationToken.java
  18. 49
    9
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubIdentityProvider.java
  19. 3
    2
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubModule.java
  20. 4
    4
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubRestClient.java
  21. 4
    4
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java
  22. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubAppConfiguration.java
  23. 1
    3
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubAppInstallation.java
  24. 22
    2
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubBinding.java
  25. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonRepositoryCollaborator.java
  26. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonRepositoryTeam.java
  27. 11
    9
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/client/GithubApplicationClient.java
  28. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/client/package-info.java
  29. 35
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/scribe/ScribeServiceBuilder.java
  30. 23
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/scribe/package-info.java
  31. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/AccessToken.java
  32. 1
    1
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/UserAccessToken.java
  33. 23
    0
      server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/package-info.java
  34. 1
    1
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/AppInstallationTokenTest.java
  35. 131
    21
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
  36. 1
    1
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubModuleTest.java
  37. 1
    1
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/GithubAppConfigurationTest.java
  38. 44
    7
      server/sonar-auth-github/src/test/java/org/sonar/auth/github/IntegrationTest.java
  39. 3
    3
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java
  40. 2
    2
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ListGithubOrganizationsActionIT.java
  41. 1
    1
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ListGithubRepositoriesActionIT.java
  42. 1
    1
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java
  43. 2
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
  44. 4
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ListGithubOrganizationsAction.java
  45. 4
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ListGithubRepositoriesAction.java
  46. 2
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreationParameters.java
  47. 5
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreator.java
  48. 4
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreatorFactory.java
  49. 2
    2
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GithubProjectCreatorFactoryTest.java
  50. 5
    5
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GithubProjectCreatorTest.java

+ 1
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/ApplicationHttpClient.java View File

@@ -21,7 +21,7 @@ package org.sonar.alm.client;

import java.io.IOException;
import java.util.Optional;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;


+ 1
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericApplicationHttpClient.java View File

@@ -37,7 +37,7 @@ import okhttp3.ResponseBody;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonarqube.ws.client.OkHttpClientBuilder;

import static com.google.common.base.Preconditions.checkArgument;

+ 1
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericPaginatedHttpClient.java View File

@@ -26,7 +26,7 @@ import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;

import static java.lang.String.format;


+ 1
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/PaginatedHttpClient.java View File

@@ -21,7 +21,7 @@ package org.sonar.alm.client;

import java.util.List;
import java.util.function.Function;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;

public interface PaginatedHttpClient {
<E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer);

+ 20
- 17
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java View File

@@ -39,20 +39,23 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.ApplicationHttpClient;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.github.GithubBinding.GsonGithubRepository;
import org.sonar.alm.client.github.GithubBinding.GsonInstallations;
import org.sonar.alm.client.github.GithubBinding.GsonRepositorySearch;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.config.GithubAppInstallation;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.GithubBinding;
import org.sonar.auth.github.GithubBinding.GsonGithubRepository;
import org.sonar.auth.github.GithubBinding.GsonInstallations;
import org.sonar.auth.github.GithubBinding.GsonRepositorySearch;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryTeam;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppInstallation;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.alm.client.github.security.AppToken;
import org.sonar.alm.client.github.security.GithubAppSecurity;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.alm.client.gitlab.GsonApp;
import org.sonar.api.internal.apachecommons.lang.StringUtils;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.server.exceptions.ServerException;
import org.sonarqube.ws.client.HttpException;

@@ -193,11 +196,11 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
return organizations;
}

organizations.setTotal(gsonInstallations.get().totalCount);
if (gsonInstallations.get().installations != null) {
organizations.setOrganizations(gsonInstallations.get().installations.stream()
.map(gsonInstallation -> new Organization(gsonInstallation.account.id, gsonInstallation.account.login, null, null, null, null, null,
gsonInstallation.targetType))
organizations.setTotal(gsonInstallations.get().getTotalCount());
if (gsonInstallations.get().getInstallations() != null) {
organizations.setOrganizations(gsonInstallations.get().getInstallations().stream()
.map(gsonInstallation -> new Organization(gsonInstallation.getAccount().getId(), gsonInstallation.getAccount().getLogin(), null, null, null, null, null,
gsonInstallation.getTargetType()))
.toList());
}

@@ -267,10 +270,10 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
return repositories;
}

repositories.setTotal(gsonRepositories.get().totalCount);
repositories.setTotal(gsonRepositories.get().getTotalCount());

if (gsonRepositories.get().items != null) {
repositories.setRepositories(gsonRepositories.get().items.stream()
if (gsonRepositories.get().getItems() != null) {
repositories.setRepositories(gsonRepositories.get().getItems().stream()
.map(GsonGithubRepository::toRepository)
.toList());
}

+ 2
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java View File

@@ -20,10 +20,11 @@
package org.sonar.alm.client.github;

import java.util.Optional;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.db.alm.setting.AlmSettingDto;

import static org.apache.commons.lang.StringUtils.isBlank;

+ 5
- 3
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java View File

@@ -22,15 +22,17 @@ package org.sonar.alm.client.github.config;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubBinding.Permissions;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppInstallation;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.GithubBinding.Permissions;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.github.GitHubSettings;
import org.sonarqube.ws.client.HttpException;

import static java.lang.Long.parseLong;
import static org.sonar.alm.client.github.GithubBinding.GsonApp;
import static org.sonar.auth.github.GithubBinding.GsonApp;
import static org.sonar.alm.client.github.config.ConfigCheckResult.ApplicationStatus;
import static org.sonar.alm.client.github.config.ConfigCheckResult.ConfigStatus;
import static org.sonar.alm.client.github.config.ConfigCheckResult.InstallationStatus;

+ 1
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/security/AppToken.java View File

@@ -20,6 +20,7 @@
package org.sonar.alm.client.github.security;

import javax.annotation.concurrent.Immutable;
import org.sonar.auth.github.security.AccessToken;

import static java.util.Objects.requireNonNull;


+ 1
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabToken.java View File

@@ -20,7 +20,7 @@
package org.sonar.alm.client.gitlab;

import java.util.Objects;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;

public class GitlabToken implements AccessToken {
private final String token;

+ 1
- 1
server/sonar-alm-client/src/test/java/org/sonar/alm/client/GenericPaginatedHttpClientImplTest.java View File

@@ -33,7 +33,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.event.Level;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.api.testfixtures.log.LogTester;

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

+ 2
- 2
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GenericApplicationHttpClientTest.java View File

@@ -41,8 +41,8 @@ import org.sonar.alm.client.GenericApplicationHttpClient;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.ApplicationHttpClient.Response;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;


+ 9
- 6
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java View File

@@ -37,19 +37,22 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.slf4j.event.Level;
import org.sonar.alm.client.ApplicationHttpClient.RateLimit;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.config.GithubAppInstallation;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryTeam;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppInstallation;
import org.sonar.auth.github.GithubBinding;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.alm.client.github.security.AppToken;
import org.sonar.alm.client.github.security.GithubAppSecurity;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.testfixtures.log.LogAndArguments;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.auth.github.GsonRepositoryPermissions;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonarqube.ws.client.HttpException;

import static java.net.HttpURLConnection.HTTP_CREATED;

+ 1
- 1
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubGlobalSettingsValidatorTest.java View File

@@ -23,7 +23,7 @@ import javax.annotation.Nullable;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.db.alm.setting.ALM;

+ 5
- 3
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java View File

@@ -27,9 +27,11 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubBinding.GsonApp;
import org.sonar.alm.client.github.GithubBinding.Permissions;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppInstallation;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.GithubBinding.GsonApp;
import org.sonar.auth.github.GithubBinding.Permissions;
import org.sonar.auth.github.GitHubSettings;
import org.sonarqube.ws.client.HttpException;


+ 1
- 1
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/security/GithubAppSecurityImplTest.java View File

@@ -28,7 +28,7 @@ import java.time.ZoneId;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppConfiguration;

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;

+ 1
- 0
server/sonar-auth-github/build.gradle View File

@@ -20,6 +20,7 @@ dependencies {

testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'com.squareup.okhttp3:okhttp'
testImplementation 'com.tngtech.java:junit-dataprovider'
testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/AppInstallationToken.java View File

@@ -17,10 +17,10 @@
* 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.alm.client.github;
package org.sonar.auth.github;

import javax.annotation.concurrent.Immutable;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.security.AccessToken;

import static java.util.Objects.requireNonNull;


+ 49
- 9
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubIdentityProvider.java View File

@@ -23,15 +23,21 @@ 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.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
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 org.sonar.api.server.http.HttpRequest;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.scribe.ScribeServiceBuilder;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.Long.parseLong;
import static java.lang.String.format;
import static org.sonar.auth.github.GitHubSettings.DEFAULT_API_URL;

public class GitHubIdentityProvider implements OAuth2IdentityProvider {

@@ -41,12 +47,17 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider {
private final UserIdentityFactory userIdentityFactory;
private final ScribeGitHubApi scribeApi;
private final GitHubRestClient gitHubRestClient;
private final GithubApplicationClient githubAppClient;
private final ScribeServiceBuilder scribeServiceBuilder;

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

@Override
@@ -107,7 +118,8 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider {
context.verifyCsrfState();

HttpRequest request = context.getHttpRequest();
OAuth20Service scribe = newScribeBuilder(context).build(scribeApi);
OAuth20Service scribe = scribeServiceBuilder.buildScribeService(settings.clientId(), settings.clientSecret(), context.getCallbackUrl(), scribeApi);

String code = request.getParameter("code");
OAuth2AccessToken accessToken = scribe.getAccessToken(code);

@@ -128,18 +140,25 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider {
context.redirectToRequestedPage();
}

boolean isOrganizationMembershipRequired() {
return !settings.getOrganizations().isEmpty();
private void check(OAuth20Service scribe, OAuth2AccessToken accessToken, GsonUser user) throws InterruptedException, ExecutionException, IOException {
if (!isUserAuthorized(scribe, accessToken, user.getLogin())) {
String message = settings.getOrganizations().isEmpty()
? format("'%s' must be a member of at least one organization which has installed the SonarQube GitHub app", user.getLogin())
: format("'%s' must be a member of at least one organization: '%s'", user.getLogin(), String.join("', '", settings.getOrganizations().stream().sorted().toList()));
throw new UnauthorizedException(message);
}
}

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.getOrganizations())));
private boolean isUserAuthorized(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException {
if (isOrganizationMembershipRequired()) {
return isOrganizationsMember(scribe, accessToken, login);
} else {
return isMemberOfInstallationOrganization(scribe, accessToken, login);
}
}

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

private boolean isOrganizationsMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException {
@@ -151,6 +170,27 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider {
return false;
}

private boolean isMemberOfInstallationOrganization(OAuth20Service scribe, OAuth2AccessToken accessToken, String login)
throws IOException, ExecutionException, InterruptedException {
GithubAppConfiguration githubAppConfiguration = githubAppConfiguration();
List<GithubAppInstallation> githubAppInstallations = githubAppClient.getWhitelistedGithubAppInstallations(githubAppConfiguration);
for (GithubAppInstallation installation : githubAppInstallations) {
if (gitHubRestClient.isOrganizationMember(scribe, accessToken, installation.organizationName(), login)) {
return true;
}
}
return false;
}

private GithubAppConfiguration githubAppConfiguration() {
String apiEndpoint = Optional.ofNullable(settings.apiURL()).orElse(DEFAULT_API_URL);
try {
return new GithubAppConfiguration(parseLong(settings.appId()), settings.privateKey(), apiEndpoint);
} catch (NumberFormatException numberFormatException) {
throw new IllegalStateException("Github configuration is not complete. Please check your configuration under the Authentication > GitHub tab");
}
}

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

+ 3
- 2
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubModule.java View File

@@ -21,6 +21,7 @@ package org.sonar.auth.github;

import java.util.List;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.auth.github.scribe.ScribeServiceBuilder;
import org.sonar.core.platform.Module;

import static org.sonar.auth.github.GitHubSettings.definitions;
@@ -30,8 +31,8 @@ public class GitHubModule extends Module {
GitHubIdentityProvider.class,
GitHubRestClient.class,
UserIdentityFactoryImpl.class,
ScribeGitHubApi.class
);
ScribeGitHubApi.class,
ScribeServiceBuilder.class);

@Override
protected void configureModule() {

+ 4
- 4
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubRestClient.java View File

@@ -45,13 +45,13 @@ public class GitHubRestClient {
this.settings = settings;
}

GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException {
public 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 {
public 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);
@@ -62,7 +62,7 @@ public class GitHubRestClient {
.orElse(null);
}

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

@@ -74,7 +74,7 @@ public class GitHubRestClient {
*
* @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)
public 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);

+ 4
- 4
server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java View File

@@ -81,11 +81,11 @@ public class GitHubSettings implements DevOpsPlatformSettings {
this.dbClient = dbClient;
}

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

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

@@ -101,11 +101,11 @@ public class GitHubSettings implements DevOpsPlatformSettings {
return configuration.getBoolean(ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty();
}

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

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


server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppConfiguration.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubAppConfiguration.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.config;
package org.sonar.auth.github;

import com.google.common.base.MoreObjects;
import java.util.regex.Pattern;

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubAppInstallation.java View File

@@ -17,8 +17,6 @@
* 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.alm.client.github.config;

import org.sonar.alm.client.github.GithubBinding;
package org.sonar.auth.github;

public record GithubAppInstallation(String installationId, String organizationName, GithubBinding.Permissions permissions, boolean isSuspended) {}

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubBinding.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/GithubBinding.java View File

@@ -17,14 +17,14 @@
* 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.alm.client.github;
package org.sonar.auth.github;

import com.google.gson.annotations.SerializedName;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import static org.sonar.alm.client.github.GithubApplicationClient.Repository;
import static org.sonar.auth.github.client.GithubApplicationClient.Repository;

public class GithubBinding {

@@ -71,6 +71,14 @@ public class GithubBinding {
// recommended:
// http://stackoverflow.com/a/18645370/229031
}

public int getTotalCount() {
return totalCount;
}

public List<GsonInstallation> getInstallations() {
return installations;
}
}

public static class GsonInstallation {
@@ -133,6 +141,10 @@ public class GithubBinding {
// http://stackoverflow.com/a/18645370/229031
}

public long getId() {
return id;
}

public String getLogin() {
return login;
}
@@ -282,6 +294,14 @@ public class GithubBinding {
// recommended:
// http://stackoverflow.com/a/18645370/229031
}
public int getTotalCount() {
return totalCount;
}

public List<GsonGithubRepository> getItems() {
return items;
}

}

public static class GsonGithubRepository {

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/api/GsonRepositoryCollaborator.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonRepositoryCollaborator.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.api;
package org.sonar.auth.github;

import com.google.gson.annotations.SerializedName;
import org.sonar.auth.github.GsonRepositoryPermissions;

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/api/GsonRepositoryTeam.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/GsonRepositoryTeam.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.api;
package org.sonar.auth.github;

import com.google.gson.annotations.SerializedName;
import org.sonar.auth.github.GsonRepositoryPermissions;

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/client/GithubApplicationClient.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.auth.github.client;

import com.google.gson.annotations.SerializedName;
import java.util.List;
@@ -26,13 +26,15 @@ import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.config.GithubAppInstallation;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.GithubAppInstallation;
import org.sonar.auth.github.GithubBinding;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryTeam;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.auth.github.security.UserAccessToken;

@ServerSide
public interface GithubApplicationClient {
@@ -105,7 +107,7 @@ public interface GithubApplicationClient {
private List<Repository> repositories;

public Repositories() {
//nothing to do
// nothing to do
}

public int getTotal() {
@@ -204,7 +206,7 @@ public interface GithubApplicationClient {
private List<Organization> organizations;

public Organizations() {
//nothing to do
// nothing to do
}

public int getTotal() {

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/api/package-info.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/client/package-info.java View File

@@ -18,6 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
package org.sonar.alm.client.github.api;
package org.sonar.auth.github.client;

import javax.annotation.ParametersAreNonnullByDefault;

+ 35
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/scribe/ScribeServiceBuilder.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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.scribe;

import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.oauth.OAuth20Service;
import org.sonar.auth.github.ScribeGitHubApi;

public class ScribeServiceBuilder {


public OAuth20Service buildScribeService(String clientId, String clientSecret, String callbackUrl, ScribeGitHubApi scribeApi) {
return new ServiceBuilder(clientId)
.apiSecret(clientSecret)
.callback(callbackUrl)
.build(scribeApi);
}
}

+ 23
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/scribe/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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.scribe;

import javax.annotation.ParametersAreNonnullByDefault;

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/security/AccessToken.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/AccessToken.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.security;
package org.sonar.auth.github.security;

/**
* Token used to authenticate requests to Github API

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/security/UserAccessToken.java → server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/UserAccessToken.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.security;
package org.sonar.auth.github.security;

public class UserAccessToken implements AccessToken {


+ 23
- 0
server/sonar-auth-github/src/main/java/org/sonar/auth/github/security/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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.security;

import javax.annotation.ParametersAreNonnullByDefault;

server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java → server/sonar-auth-github/src/test/java/org/sonar/auth/github/AppInstallationTokenTest.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.auth.github;

import org.junit.Test;


+ 131
- 21
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java View File

@@ -19,28 +19,49 @@
*/
package org.sonar.auth.github;

import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.oauth.OAuth20Service;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
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 org.sonar.api.server.http.HttpRequest;
import org.sonar.auth.github.GithubBinding.Permissions;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.scribe.ScribeServiceBuilder;
import org.sonar.db.DbClient;
import org.sonar.server.property.InternalProperties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.CallbackContext;
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.InitContext;
import static org.sonar.auth.github.GitHubSettings.APP_ID;
import static org.sonar.auth.github.GitHubSettings.CLIENT_ID;
import static org.sonar.auth.github.GitHubSettings.CLIENT_SECRET;
import static org.sonar.auth.github.GitHubSettings.ENABLED;
import static org.sonar.auth.github.GitHubSettings.ORGANIZATIONS;
import static org.sonar.auth.github.GitHubSettings.PRIVATE_KEY;

public class GitHubIdentityProviderTest {


private MapSettings settings = new MapSettings();
private InternalProperties internalProperties = mock(InternalProperties.class);
private GitHubSettings gitHubSettings = new GitHubSettings(settings.asConfig(), internalProperties, mock(DbClient.class));
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);
private GitHubRestClient gitHubRestClient = mock();
private GithubApplicationClient githubAppClient = mock();

private ScribeServiceBuilder scribeServiceBuilder = mock();
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient, githubAppClient, scribeServiceBuilder);

@Test
public void check_fields() {
@@ -72,7 +93,7 @@ public class GitHubIdentityProviderTest {
@Test
public void init() {
setSettings(true);
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);
InitContext context = mock(InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/");
@@ -91,7 +112,7 @@ public class GitHubIdentityProviderTest {
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);
InitContext context = mock(InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");

@@ -109,7 +130,7 @@ public class GitHubIdentityProviderTest {
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);
InitContext context = mock(InitContext.class);
when(context.generateCsrfState()).thenReturn("state");
when(context.getCallbackUrl()).thenReturn("http://localhost/callback");

@@ -126,7 +147,7 @@ public class GitHubIdentityProviderTest {
@Test
public void fail_to_init_when_disabled() {
setSettings(false);
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class);
InitContext context = mock(InitContext.class);

assertThatThrownBy(() -> underTest.init(context))
.isInstanceOf(IllegalStateException.class)
@@ -155,28 +176,117 @@ public class GitHubIdentityProviderTest {
}

@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();
public void callback_whenOrganizationsAreDefinedAndUserBelongsToOne_shouldAuthenticateAndRedirect() throws IOException, ExecutionException, InterruptedException {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserBelongingToOrganization(userIdentity);

settings.setProperty(ORGANIZATIONS, "organization1,organization2");
underTest.callback(context);

verify(context).authenticate(userIdentity);
verify(context).redirectToRequestedPage();
}

@Test
public void organization_membership_not_required() {
public void callback_whenOrganizationsAreDefinedAndDoesntBelongToOne_shouldThrow() throws IOException, ExecutionException, InterruptedException {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserNotBelongingToOrganization(userIdentity);

settings.setProperty(ORGANIZATIONS, "organization1,organization2");

assertThatThrownBy(() -> underTest.callback(context))
.isInstanceOf(UnauthorizedException.class)
.hasMessage("'login' must be a member of at least one organization: 'organization1', 'organization2'");
}

@Test
public void callback_whenOrganizationsAreNotDefinedAndUserBelongsToInstallationOrganization_shouldAuthenticateAndRedirect()
throws IOException, ExecutionException, InterruptedException {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserBelongingToOrganization(userIdentity);

mockInstallations();

underTest.callback(context);

verify(context).authenticate(userIdentity);
verify(context).redirectToRequestedPage();
}

@Test
public void callback_whenOrganizationsAreNotDefinedAndUserDoesntBelongToInstallationOrganization_shouldThrow() throws IOException, ExecutionException, InterruptedException {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserNotBelongingToOrganization(userIdentity);

mockInstallations();

assertThatThrownBy(() -> underTest.callback(context))
.isInstanceOf(UnauthorizedException.class)
.hasMessage("'login' must be a member of at least one organization which has installed the SonarQube GitHub app");
}

private CallbackContext mockUserBelongingToOrganization(UserIdentity userIdentity) throws IOException, InterruptedException, ExecutionException {
setSettings(true);
settings.setProperty("sonar.auth.github.organizations", "");
assertThat(underTest.isOrganizationMembershipRequired()).isFalse();
CallbackContext context = mock();
HttpRequest httpRequest = mock();
OAuth20Service scribeService = mock();
GsonUser user = new GsonUser("id", "login", "name", "email");

OAuth2AccessToken accessToken = mockAccessToken(scribeService, context, httpRequest, user);

when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization1", "login")).thenReturn(false);
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization2", "login")).thenReturn(true);

when(userIdentityFactory.create(user, "email", null)).thenReturn(userIdentity);
return context;
}

private CallbackContext mockUserNotBelongingToOrganization(UserIdentity userIdentity) throws IOException, InterruptedException, ExecutionException {
setSettings(true);
CallbackContext context = mock();
HttpRequest httpRequest = mock();
OAuth20Service scribeService = mock();
GsonUser user = new GsonUser("id", "login", "name", "email");

OAuth2AccessToken accessToken = mockAccessToken(scribeService, context, httpRequest, user);

when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization1", "login")).thenReturn(false);
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization2", "login")).thenReturn(false);

when(userIdentityFactory.create(user, "email", null)).thenReturn(userIdentity);
return context;
}

private OAuth2AccessToken mockAccessToken(OAuth20Service scribeService, CallbackContext context, HttpRequest httpRequest, GsonUser user)
throws IOException, InterruptedException, ExecutionException {
String callbackUrl = "http://localhost/callback";
when(scribeServiceBuilder.buildScribeService("id", "secret", callbackUrl, scribeApi)).thenReturn(scribeService);
when(context.getHttpRequest()).thenReturn(httpRequest);
when(context.getCallbackUrl()).thenReturn(callbackUrl);

when(httpRequest.getParameter("code")).thenReturn("code");
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
when(scribeService.getAccessToken("code")).thenReturn(accessToken);

when(gitHubRestClient.getUser(scribeService, accessToken)).thenReturn(user);
return accessToken;
}

private void mockInstallations() {
when(githubAppClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of(
new GithubAppInstallation("1", "organization1", new Permissions(), false),
new GithubAppInstallation("2", "organization2", new Permissions(), false)));
}

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.enabled", true);
settings.setProperty(CLIENT_ID, "id");
settings.setProperty(CLIENT_SECRET, "secret");
settings.setProperty(ENABLED, true);
settings.setProperty(APP_ID, "1");
settings.setProperty(PRIVATE_KEY, "private");
} else {
settings.setProperty("sonar.auth.github.enabled", false);
settings.setProperty(ENABLED, false);
}
}
}

+ 1
- 1
server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubModuleTest.java View File

@@ -30,7 +30,7 @@ public class GitHubModuleTest {
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new GitHubModule().configure(container);
assertThat(container.getAddedObjects()).hasSize(15);
assertThat(container.getAddedObjects()).hasSize(16);
}

}

server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubAppConfigurationTest.java → server/sonar-auth-github/src/test/java/org/sonar/auth/github/GithubAppConfigurationTest.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github.config;
package org.sonar.auth.github;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;

+ 44
- 7
server/sonar-auth-github/src/test/java/org/sonar/auth/github/IntegrationTest.java View File

@@ -22,6 +22,7 @@ package org.sonar.auth.github;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletRequest;
@@ -40,6 +41,8 @@ import org.sonar.api.server.authentication.UserIdentity;
import org.sonar.api.server.http.HttpRequest;
import org.sonar.api.server.http.HttpResponse;
import org.sonar.api.utils.System2;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.scribe.ScribeServiceBuilder;
import org.sonar.db.DbTester;
import org.sonar.server.http.JavaxHttpRequest;
import org.sonar.server.property.InternalProperties;
@@ -47,7 +50,9 @@ import org.sonar.server.property.InternalPropertiesImpl;

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

@@ -69,9 +74,13 @@ public class IntegrationTest {
private ScribeGitHubApi scribeApi = new ScribeGitHubApi(gitHubSettings);
private GitHubRestClient gitHubRestClient = new GitHubRestClient(gitHubSettings);

private GithubApplicationClient githubAppClient = mock();

private ScribeServiceBuilder scribeServiceBuilder = new ScribeServiceBuilder();

private String gitHubUrl;

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

@Before
public void enable() {
@@ -81,6 +90,8 @@ public class IntegrationTest {
settings.setProperty("sonar.auth.github.enabled", true);
settings.setProperty("sonar.auth.github.apiUrl", gitHubUrl);
settings.setProperty("sonar.auth.github.webUrl", gitHubUrl);
settings.setProperty("sonar.auth.github.appId", "1");
settings.setProperty("sonar.auth.github.privateKey.secured", "private_key");
}

/**
@@ -115,9 +126,15 @@ public class IntegrationTest {
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));
// response of api.github.com/orgs/second_org/members/user
github.enqueue(new MockResponse().setResponseCode(204));

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
mockInstallations();

underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
@@ -146,6 +163,8 @@ public class IntegrationTest {
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/orgs/first_org/members/user
github.enqueue(new MockResponse().setResponseCode(204));
// response of api.github.com/user/emails
github.enqueue(new MockResponse().setBody(
"[\n" +
@@ -163,6 +182,8 @@ public class IntegrationTest {

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
mockInstallations();

underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
@@ -178,11 +199,16 @@ public class IntegrationTest {
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/orgs/first_org/members/user
github.enqueue(new MockResponse().setResponseCode(204));
// response of api.github.com/user/emails
github.enqueue(new MockResponse().setBody("[]"));


HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
mockInstallations();

underTest.callback(callbackContext);

assertThat(callbackContext.csrfStateVerified.get()).isTrue();
@@ -215,6 +241,8 @@ public class IntegrationTest {
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(204));
// response of api.github.com/user/teams
github.enqueue(new MockResponse().setBody("[\n" +
" {\n" +
@@ -227,6 +255,8 @@ public class IntegrationTest {

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
mockInstallations();

underTest.callback(callbackContext);

assertThat(callbackContext.userIdentity.getGroups()).containsOnly("SonarSource/developers");
@@ -239,6 +269,8 @@ public class IntegrationTest {
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(204));
// 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\"")
@@ -263,6 +295,8 @@ public class IntegrationTest {

HttpServletRequest request = newRequest("the-verifier-code");
DumbCallbackContext callbackContext = new DumbCallbackContext(request);
mockInstallations();

underTest.callback(callbackContext);

assertThat(new TreeSet<>(callbackContext.userIdentity.getGroups())).containsOnly("SonarQubeCommunity/sonarsource-developers", "SonarSource/developers");
@@ -316,12 +350,9 @@ public class IntegrationTest {

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

@Test
@@ -378,6 +409,12 @@ public class IntegrationTest {
return request;
}

private void mockInstallations() {
when(githubAppClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of(
new GithubAppInstallation("1", "first_org", new GithubBinding.Permissions(), false),
new GithubAppInstallation("2", "second_org", new GithubBinding.Permissions(), false)));
}

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

+ 3
- 3
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java View File

@@ -25,10 +25,10 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;

+ 2
- 2
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ListGithubOrganizationsActionIT.java View File

@@ -24,9 +24,9 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.api.server.ws.WebService;

+ 1
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ListGithubRepositoriesActionIT.java View File

@@ -22,7 +22,7 @@ package org.sonar.server.almintegration.ws.github;
import java.util.stream.Stream;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;

+ 1
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java View File

@@ -27,7 +27,7 @@ import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.api.utils.System2;
import org.sonar.auth.github.GitHubSettings;

+ 2
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java View File

@@ -21,8 +21,8 @@ package org.sonar.server.almintegration.ws.github;

import java.util.Optional;
import javax.inject.Inject;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;

+ 4
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ListGithubOrganizationsAction.java View File

@@ -21,11 +21,11 @@ package org.sonar.server.almintegration.ws.github;

import java.util.List;
import java.util.Optional;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClient.Organization;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.client.GithubApplicationClient.Organization;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.api.server.ws.Request;

+ 4
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ListGithubRepositoriesAction.java View File

@@ -27,11 +27,11 @@ import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClient.Repository;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.client.GithubApplicationClient.Repository;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.auth.github.security.UserAccessToken;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;

+ 2
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreationParameters.java View File

@@ -20,8 +20,8 @@
package org.sonar.server.almsettings.ws;

import javax.annotation.Nullable;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.server.user.UserSession;


+ 5
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreator.java View File

@@ -23,12 +23,12 @@ import java.util.Optional;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryTeam;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.api.web.UserRole;
import org.sonar.auth.github.GsonRepositoryPermissions;
import org.sonar.db.DbClient;

+ 4
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GithubProjectCreatorFactory.java View File

@@ -23,12 +23,12 @@ import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.GithubAppConfiguration;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.db.DbClient;

+ 2
- 2
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GithubProjectCreatorFactoryTest.java View File

@@ -28,8 +28,8 @@ import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.auth.github.GitHubSettings;

+ 5
- 5
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GithubProjectCreatorTest.java View File

@@ -31,11 +31,11 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.auth.github.AppInstallationToken;
import org.sonar.auth.github.client.GithubApplicationClient;
import org.sonar.auth.github.GsonRepositoryCollaborator;
import org.sonar.auth.github.GsonRepositoryTeam;
import org.sonar.auth.github.security.AccessToken;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.web.UserRole;
import org.sonar.alm.client.github.GithubPermissionConverter;

Loading…
Cancel
Save