Browse Source

SONAR-19346 GitHub config check available in Community Edition

tags/10.1.0.73491
Antoine Vigneau 1 year ago
parent
commit
c1966338ee
16 changed files with 1093 additions and 2 deletions
  1. 1
    0
      server/sonar-alm-client/build.gradle
  2. 9
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
  3. 75
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
  4. 51
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java
  5. 24
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java
  6. 167
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java
  7. 138
    1
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
  8. 294
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java
  9. 107
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/config/CheckActionIT.java
  10. 2
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
  11. 25
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/GithubProvisioningAction.java
  12. 46
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/GithubProvisioningWs.java
  13. 69
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/config/CheckAction.java
  14. 32
    0
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/github/config/check-example.json
  15. 49
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/github/GithubProvisioningWsTest.java
  16. 4
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

+ 1
- 0
server/sonar-alm-client/build.gradle View File

@@ -11,6 +11,7 @@ dependencies {
api 'com.auth0:java-jwt'
api 'org.bouncycastle:bcpkix-jdk15on:1.70'
api 'org.sonarsource.api.plugin:sonar-plugin-api'
api project(':server:sonar-auth-github')

testImplementation project(':sonar-plugin-api-impl')


+ 9
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java View File

@@ -26,6 +26,7 @@ import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
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;
@@ -43,11 +44,19 @@ public interface GithubApplicationClient {
*/
UserAccessToken createUserAccessToken(String appUrl, String clientId, String clientSecret, String code);

GithubBinding.GsonApp getApp(GithubAppConfiguration githubAppConfiguration);

/**
* Lists all the organizations accessible to the access token provided.
*/
Organizations listOrganizations(String appUrl, AccessToken accessToken, int page, int pageSize);

/**
* Retrieve all installations of the GitHub app, filtering out the ones not whitelisted in GitHub Settings (if set)
* @throws IllegalArgumentException if one of the arguments is invalid (for example, wrong private key)
*/
List<GithubAppInstallation> getWhitelistedGithubAppInstallations(GithubAppConfiguration githubAppConfiguration);

/**
* Lists all the repositories of the provided organization accessible to the access token provided.
*/

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

@@ -29,6 +29,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse;
@@ -36,6 +37,7 @@ 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.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.AppToken;
import org.sonar.alm.client.github.security.GithubAppSecurity;
@@ -44,10 +46,14 @@ import org.sonar.alm.client.gitlab.GsonApp;
import org.sonar.api.internal.apachecommons.lang.StringUtils;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.server.exceptions.ServerException;
import org.sonarqube.ws.client.HttpException;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;

@@ -61,10 +67,12 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

protected final GithubApplicationHttpClient appHttpClient;
protected final GithubAppSecurity appSecurity;
private final GitHubSettings gitHubSettings;

public GithubApplicationClientImpl(GithubApplicationHttpClient appHttpClient, GithubAppSecurity appSecurity) {
public GithubApplicationClientImpl(GithubApplicationHttpClient appHttpClient, GithubAppSecurity appSecurity, GitHubSettings gitHubSettings) {
this.appHttpClient = appHttpClient;
this.appSecurity = appSecurity;
this.gitHubSettings = gitHubSettings;
}

private static void checkPageArgs(int page, int pageSize) {
@@ -160,6 +168,53 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}
}

@Override
public List<GithubAppInstallation> getWhitelistedGithubAppInstallations(GithubAppConfiguration githubAppConfiguration) {
GithubBinding.GsonInstallation[] gsonAppInstallations = fetchAppInstallationsFromGithub(githubAppConfiguration);
Set<String> allowedOrganizations = gitHubSettings.getOrganizations();
return convertToGithubAppInstallationAndFilterWhitelisted(gsonAppInstallations, allowedOrganizations);
}

private static List<GithubAppInstallation> convertToGithubAppInstallationAndFilterWhitelisted(GithubBinding.GsonInstallation[] gsonAppInstallations,
Set<String> allowedOrganizations) {
return Arrays.stream(gsonAppInstallations)
.filter(appInstallation -> appInstallation.getAccount().getType().equalsIgnoreCase("Organization"))
.map(GithubApplicationClientImpl::toGithubAppInstallation)
.filter(appInstallation -> isOrganizationWhiteListed(allowedOrganizations, appInstallation.organizationName()))
.toList();
}

private static GithubAppInstallation toGithubAppInstallation(GithubBinding.GsonInstallation gsonInstallation) {
return new GithubAppInstallation(
Long.toString(gsonInstallation.getId()),
gsonInstallation.getAccount().getLogin(),
gsonInstallation.getPermissions(),
org.apache.commons.lang.StringUtils.isNotEmpty(gsonInstallation.getSuspendedAt()));
}

private static boolean isOrganizationWhiteListed(Set<String> allowedOrganizations, String organizationName) {
return allowedOrganizations.isEmpty() || allowedOrganizations.contains(organizationName);
}

private GithubBinding.GsonInstallation[] fetchAppInstallationsFromGithub(GithubAppConfiguration githubAppConfiguration) {
AppToken appToken = appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey());
String endpoint = "/app/installations";
return get(githubAppConfiguration.getApiEndpoint(), appToken, endpoint,
GithubBinding.GsonInstallation[].class).orElseThrow(
() -> new IllegalStateException("An error occurred when retrieving your GitHup App installations. "
+ "It might be related to your GitHub App configuration or a connectivity problem."));
}

protected <T> Optional<T> get(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
GetResponse response = appHttpClient.get(baseUrl, token, endPoint);
return handleResponse(response, endPoint, gsonClass);
} catch (Exception e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + endPoint, e);
return Optional.empty();
}
}

@Override
public Repositories listRepositories(String appUrl, AccessToken accessToken, String organization, @Nullable String query, int page, int pageSize) {
checkPageArgs(page, pageSize);
@@ -242,6 +297,25 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}
}

@Override
public GithubBinding.GsonApp getApp(GithubAppConfiguration githubAppConfiguration) {
AppToken appToken = appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey());
String endpoint = "/app";
return getOrThrowIfNotHttpOk(githubAppConfiguration.getApiEndpoint(), appToken, endpoint, GithubBinding.GsonApp.class);
}

private <T> T getOrThrowIfNotHttpOk(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
GetResponse response = appHttpClient.get(baseUrl, token, endPoint);
if (response.getCode() != HTTP_OK) {
throw new HttpException(baseUrl + endPoint, response.getCode(), response.getContent().orElse(""));
}
return handleResponse(response, endPoint, gsonClass).orElseThrow(() -> new ServerException(HTTP_INTERNAL_ERROR, "Http response withuot content"));
} catch (IOException e) {
throw new ServerException(HTTP_INTERNAL_ERROR, e.getMessage());
}
}


protected static <T> Optional<T> handleResponse(GithubApplicationHttpClient.Response response, String endPoint, Class<T> gsonClass) {
try {

+ 51
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.github.config;

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

public record ConfigCheckResult(@SerializedName("application") ApplicationStatus application, @SerializedName("installations") List<InstallationStatus> installations) {

public record ConfigStatus(@SerializedName("status") String status, @Nullable @SerializedName("errorMessage") String errorMessage) {

public static final String SUCCESS_STATUS = "SUCCESS";
public static final String FAILED_STATUS = "FAILED";

public static final ConfigStatus SUCCESS = new ConfigStatus(SUCCESS_STATUS);

public ConfigStatus(String status) {
this(status, null);
}

public static ConfigStatus failed(String errorMessage) {
return new ConfigStatus(FAILED_STATUS, errorMessage);
}

}

public record ApplicationStatus(@SerializedName("jit") ConfigStatus jit, @SerializedName("autoProvisioning") ConfigStatus autoProvisioning) {
}

public record InstallationStatus(@SerializedName("organization") String organization, @SerializedName("jit") ConfigStatus jit,
@SerializedName("autoProvisioning") ConfigStatus autoProvisioning) {
}
}

+ 24
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java View File

@@ -0,0 +1,24 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.github.config;

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

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

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

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

@ServerSide
@ComputeEngineSide
public class GithubProvisioningConfigValidator {

private static final ConfigStatus APP_NOT_FOUND_STATUS = ConfigStatus.failed("Github App not found");
private static final String MEMBERS_PERMISSION = "Organization permissions -> Members";

private static final String EMAILS_PERMISSION = "Account permissions -> Email addresses";

private static final ConfigStatus INVALID_APP_CONFIG_STATUS = ConfigStatus.failed("The GitHub App configuration is not complete.");
private static final ConfigStatus INVALID_APP_ID_STATUS = ConfigStatus.failed("GitHub App ID must be a number.");
private static final ConfigStatus SUSPENDED_INSTALLATION_STATUS = ConfigStatus.failed("Installation suspended");
private static final ConfigStatus NO_INSTALLATION_FOUND_STATUS = ConfigStatus.failed(
"The GitHub App is not installed on any organizations or the organization is not white-listed.");
private static final ConfigCheckResult NO_INSTALLATIONS_RESULT = new ConfigCheckResult(
new ApplicationStatus(
NO_INSTALLATION_FOUND_STATUS,
NO_INSTALLATION_FOUND_STATUS),
List.of());

private final GithubApplicationClient githubClient;
private final GitHubSettings gitHubSettings;

public GithubProvisioningConfigValidator(GithubApplicationClient githubClient, GitHubSettings gitHubSettings) {
this.githubClient = githubClient;
this.gitHubSettings = gitHubSettings;
}

public ConfigCheckResult checkConfig() {
Optional<Long> appId = getAppId();
if (appId.isEmpty()) {
return failedApplicationStatus(INVALID_APP_ID_STATUS);
}
GithubAppConfiguration githubAppConfiguration = new GithubAppConfiguration(appId.get(), gitHubSettings.privateKey(), gitHubSettings.apiURLOrDefault());
return checkConfig(githubAppConfiguration);
}

private Optional<Long> getAppId() {
try {
return Optional.of(parseLong(gitHubSettings.appId()));
} catch (NumberFormatException numberFormatException) {
return Optional.empty();
}
}

public ConfigCheckResult checkConfig(GithubAppConfiguration githubAppConfiguration) {
if (!githubAppConfiguration.isComplete()) {
return failedApplicationStatus(INVALID_APP_CONFIG_STATUS);
}

try {
GsonApp app = githubClient.getApp(githubAppConfiguration);
return checkNonEmptyConfig(githubAppConfiguration, app);
} catch (HttpException e) {
return failedApplicationStatus(
ConfigStatus.failed("Error response from GitHub: " + e.getMessage()));
}
}

private static ConfigCheckResult failedApplicationStatus(ConfigStatus configStatus) {
return new ConfigCheckResult(new ApplicationStatus(configStatus, configStatus), List.of());
}

private ConfigCheckResult checkNonEmptyConfig(GithubAppConfiguration githubAppConfiguration, GsonApp app) {
ApplicationStatus appStatus = checkNonEmptyAppConfig(app);
List<InstallationStatus> installations = checkInstallations(githubAppConfiguration, appStatus);
if (installations.isEmpty()) {
return NO_INSTALLATIONS_RESULT;
}
return new ConfigCheckResult(
appStatus,
installations);
}

private static ApplicationStatus checkNonEmptyAppConfig(GsonApp app) {
return new ApplicationStatus(
jitAppConfigStatus(app.getPermissions()),
autoProvisioningAppConfigStatus(app.getPermissions()));
}

private static ConfigStatus jitAppConfigStatus(Permissions permissions) {
if (permissions.getEmails() == null) {
return failedStatus(List.of(EMAILS_PERMISSION));
}
return ConfigStatus.SUCCESS;
}

private static ConfigStatus autoProvisioningAppConfigStatus(Permissions permissions) {
List<String> missingPermissions = new ArrayList<>();
if (permissions.getEmails() == null) {
missingPermissions.add(EMAILS_PERMISSION);
}
if (permissions.getMembers() == null) {
missingPermissions.add(MEMBERS_PERMISSION);
}
if (missingPermissions.isEmpty()) {
return ConfigStatus.SUCCESS;
}
return failedStatus(missingPermissions);
}

private static ConfigStatus failedStatus(List<String> missingPermissions) {
return ConfigStatus.failed("Missing permissions: " + String.join(",", missingPermissions));
}

private List<InstallationStatus> checkInstallations(GithubAppConfiguration githubAppConfiguration, ApplicationStatus appStatus) {
return githubClient.getWhitelistedGithubAppInstallations(githubAppConfiguration)
.stream()
.map(installation -> statusForInstallation(installation, appStatus))
.toList();
}

private static InstallationStatus statusForInstallation(GithubAppInstallation installation, ApplicationStatus appStatus) {
if (installation.isSuspended()) {
return new InstallationStatus(installation.organizationName(), SUSPENDED_INSTALLATION_STATUS, SUSPENDED_INSTALLATION_STATUS);
}
return new InstallationStatus(
installation.organizationName(),
appStatus.jit(),
autoProvisioningInstallationConfigStatus(installation.permissions()));
}

private static ConfigStatus autoProvisioningInstallationConfigStatus(Permissions permissions) {
if (permissions.getMembers() == null) {
return failedStatus(List.of(MEMBERS_PERMISSION));
}
return ConfigStatus.SUCCESS;
}

}

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

@@ -23,19 +23,24 @@ import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
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.AppToken;
import org.sonar.alm.client.github.security.GithubAppSecurity;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.auth.github.GitHubSettings;
import org.sonarqube.ws.client.HttpException;

import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
@@ -53,12 +58,43 @@ import static org.mockito.Mockito.when;
@RunWith(DataProviderRunner.class)
public class GithubApplicationClientImplTest {

private static final String APP_JWT_TOKEN = "APP_TOKEN_JWT";
private static final String PAYLOAD_2_ORGS = """
[
{
"id": 1,
"account": {
"login": "org1",
"type": "Organization"
},
"target_type": "Organization",
"permissions": {
"members": "read",
"metadata": "read"
},
"suspended_at": "2023-05-30T08:40:55Z"
},
{
"id": 2,
"account": {
"login": "org2",
"type": "Organization"
},
"target_type": "Organization",
"permissions": {
"members": "read",
"metadata": "read"
}
}
]""";

@ClassRule
public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);

private GithubApplicationHttpClientImpl httpClient = mock(GithubApplicationHttpClientImpl.class);
private GithubAppSecurity appSecurity = mock(GithubAppSecurity.class);
private GithubAppConfiguration githubAppConfiguration = mock(GithubAppConfiguration.class);
private GitHubSettings gitHubSettings = mock(GitHubSettings.class);
private GithubApplicationClient underTest;

private String appUrl = "Any URL";
@@ -66,7 +102,7 @@ public class GithubApplicationClientImplTest {
@Before
public void setup() {
when(githubAppConfiguration.getApiEndpoint()).thenReturn(appUrl);
underTest = new GithubApplicationClientImpl(httpClient, appSecurity);
underTest = new GithubApplicationClientImpl(httpClient, appSecurity, gitHubSettings);
logTester.clear();
}

@@ -242,6 +278,31 @@ public class GithubApplicationClientImplTest {
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
}

@Test
public void getApp_returns_id() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app"))
.thenReturn(new OkGetResponse("{\"installations_count\": 2}"));

assertThat(underTest.getApp(githubAppConfiguration).getInstallationsCount()).isEqualTo(2L);
}

@Test
public void getApp_whenStatusCodeIsNotOk_shouldThrowHttpException() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app"))
.thenReturn(new ErrorGetResponse(418, "I'm a teapot"));

assertThatThrownBy(() -> underTest.getApp(githubAppConfiguration))
.isInstanceOfSatisfying(HttpException.class, httpException -> {
assertThat(httpException.code()).isEqualTo(418);
assertThat(httpException.url()).isEqualTo("Any URL/app");
assertThat(httpException.content()).isEqualTo("I'm a teapot");
});
}

@DataProvider
public static Object[][] githubServers() {
return new Object[][] {
@@ -392,6 +453,82 @@ public class GithubApplicationClientImplTest {
assertThat(organizations.getOrganizations()).extracting(GithubApplicationClient.Organization::getLogin).containsOnly("github", "octocat");
}

@Test
public void getWhitelistedGithubAppInstallations_whenWhitelistNotSpecified_doesNotFilter() throws IOException {
List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse(PAYLOAD_2_ORGS);
assertOrgDeserialization(allOrgInstallations);
}

private static void assertOrgDeserialization(List<GithubAppInstallation> orgs) {
GithubAppInstallation org1 = orgs.get(0);
assertThat(org1.installationId()).isEqualTo("1");
assertThat(org1.organizationName()).isEqualTo("org1");
assertThat(org1.permissions().getMembers()).isEqualTo("read");
assertThat(org1.isSuspended()).isTrue();

GithubAppInstallation org2 = orgs.get(1);
assertThat(org2.installationId()).isEqualTo("2");
assertThat(org2.organizationName()).isEqualTo("org2");
assertThat(org2.permissions().getMembers()).isEqualTo("read");
assertThat(org2.isSuspended()).isFalse();
}

@Test
public void getWhitelistedGithubAppInstallations_whenWhitelistSpecified_filtersWhitelistedOrgs() throws IOException {
when(gitHubSettings.getOrganizations()).thenReturn(Set.of("org2"));
List<GithubAppInstallation> orgInstallations = getGithubAppInstallationsFromGithubResponse(PAYLOAD_2_ORGS);
assertThat(orgInstallations)
.hasSize(1)
.extracting(GithubAppInstallation::organizationName)
.containsExactlyInAnyOrder("org2");
}

@Test
public void getWhitelistedGithubAppInstallations_whenEmptyResponse_shouldReturnEmpty() throws IOException {
List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse("[]");
assertThat(allOrgInstallations).isEmpty();
}

@Test
public void getWhitelistedGithubAppInstallations_whenNoOrganization_shouldReturnEmpty() throws IOException {
List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse("""
[
{
"id": 1,
"account": {
"login": "user1",
"type": "User"
},
"target_type": "User",
"permissions": {
"metadata": "read"
}
}
]""");
assertThat(allOrgInstallations).isEmpty();
}

private List<GithubAppInstallation> getGithubAppInstallationsFromGithubResponse(String content) throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app/installations"))
.thenReturn(new OkGetResponse(content));
return underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration);
}

@Test
public void getWhitelistedGithubAppInstallations_whenGithubReturnsError_shouldThrow() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app/installations"))
.thenReturn(new ErrorGetResponse());

assertThatThrownBy(() -> underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration))
.isInstanceOf(IllegalStateException.class)
.hasMessage("An error occurred when retrieving your GitHup App installations. "
+ "It might be related to your GitHub App configuration or a connectivity problem.");
}

@Test
public void listRepositories_fail_on_failure() throws IOException {
String appUrl = "https://github.sonarsource.com";

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

@@ -0,0 +1,294 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.github.config;

import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
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.GitHubSettings;
import org.sonarqube.ws.client.HttpException;

import static java.lang.Long.parseLong;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
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.alm.client.github.config.ConfigCheckResult.ConfigStatus;
import static org.sonar.alm.client.github.config.ConfigCheckResult.InstallationStatus;

@RunWith(MockitoJUnitRunner.class)
public class GithubProvisioningConfigValidatorTest {

private static final String SUCCESS_STATUS = "SUCCESS";
private static final String GITHUB_CALL_FAILED = "Error response from GitHub: GitHub call failed.";
private static final String INVALID_APP_ID_STATUS = "GitHub App ID must be a number.";
private static final String INCOMPLETE_APP_CONFIG_STATUS = "The GitHub App configuration is not complete.";
private static final String MISSING_EMAIL_PERMISSION = "Missing permissions: Account permissions -> Email addresses";
private static final String MISSING_MEMBERS_PERMISSION = "Missing permissions: Organization permissions -> Members";
private static final String MISSING_EMAILS_AND_MEMBERS_PERMISSION = "Missing permissions: Account permissions -> Email addresses,Organization permissions -> Members";
private static final String NO_INSTALLATIONS_STATUS = "The GitHub App is not installed on any organizations or the organization is not white-listed.";
private static final String SUSPENDED_INSTALLATION = "Installation suspended";

private static final ConfigStatus SUCCESS_CHECK = new ConfigStatus(SUCCESS_STATUS, null);
public static final String APP_ID = "1";
public static final String PRIVATE_KEY = "secret";
public static final String URL = "url";
@Mock
private GithubApplicationClient githubClient;

@Mock
private GitHubSettings gitHubSettings;

@InjectMocks
private GithubProvisioningConfigValidator configValidator;

@Test
public void checkConfig_whenAppIdIsNull_shouldReturnFailedAppCheck() {
when(gitHubSettings.appId()).thenReturn(null);

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(INVALID_APP_ID_STATUS));
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(INVALID_APP_ID_STATUS));
assertThat(checkResult.installations()).isEmpty();
}
@Test
public void checkConfig_whenAppIdNotValid_shouldReturnFailedAppCheck() {
when(gitHubSettings.appId()).thenReturn("not a number");

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(INVALID_APP_ID_STATUS));
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(INVALID_APP_ID_STATUS));
assertThat(checkResult.installations()).isEmpty();
}

@Test
public void checkConfig_whenGithubAppConfigurationNotComplete_shouldReturnFailedAppCheck() {
when(gitHubSettings.appId()).thenReturn(APP_ID);

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(INCOMPLETE_APP_CONFIG_STATUS));
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(INCOMPLETE_APP_CONFIG_STATUS));
assertThat(checkResult.installations()).isEmpty();
}

@Test
public void checkConfig_whenErrorWhileFetchingTheApp_shouldReturnFailedAppCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);

HttpException httpException = mock(HttpException.class);
when(httpException.getMessage()).thenReturn("GitHub call failed.");

when(githubClient.getApp(appConfigurationCaptor.capture())).thenThrow(httpException);

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(GITHUB_CALL_FAILED));
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(GITHUB_CALL_FAILED));
assertThat(checkResult.installations()).isEmpty();
}

@Test
public void checkConfig_whenAppDoesntHaveEmailsPermissions_shouldReturnFailedAppJitCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);
GsonApp githubApp = mockGithubApp(appConfigurationCaptor);

when(githubApp.getPermissions()).thenReturn(new Permissions());
mockOrganizationsWithoutPermissions(appConfigurationCaptor, "org1", "org2");

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(MISSING_EMAILS_AND_MEMBERS_PERMISSION));
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(MISSING_EMAIL_PERMISSION));
assertThat(checkResult.installations()).hasSize(2);
assertThat(checkResult.installations())
.extracting(InstallationStatus::jit, InstallationStatus::autoProvisioning)
.containsExactly(
tuple(ConfigStatus.failed(MISSING_EMAIL_PERMISSION), ConfigStatus.failed(MISSING_MEMBERS_PERMISSION)),
tuple(ConfigStatus.failed(MISSING_EMAIL_PERMISSION), ConfigStatus.failed(MISSING_MEMBERS_PERMISSION)));
verifyAppConfiguration(appConfigurationCaptor.getValue());

}

@Test
public void checkConfig_whenAppDoesntHaveMembersPermissions_shouldReturnFailedAppAutoProvisioningCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);

GsonApp githubApp = mockGithubApp(appConfigurationCaptor);
when(githubApp.getPermissions()).thenReturn(new Permissions(null, null, "read"));
mockOrganizations(appConfigurationCaptor, "org1", "org2");

ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.SUCCESS);
assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(MISSING_MEMBERS_PERMISSION));
assertThat(checkResult.installations()).hasSize(2);
verifyAppConfiguration(appConfigurationCaptor.getValue());
}

@Test
public void checkConfig_whenNoInstallationsAreReturned_shouldReturnFailedAppAutoProvisioningCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);
mockGithubAppWithValidConfig(appConfigurationCaptor);

mockOrganizationsWithoutPermissions(appConfigurationCaptor);
ConfigCheckResult checkResult = configValidator.checkConfig();

assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.failed(NO_INSTALLATIONS_STATUS));
assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.failed(NO_INSTALLATIONS_STATUS));
assertThat(checkResult.installations()).isEmpty();

verifyAppConfiguration(appConfigurationCaptor.getValue());

}

@Test
public void checkConfig_whenInstallationsDoesntHaveMembersPermissions_shouldReturnFailedAppAutoProvisioningCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);
mockGithubAppWithValidConfig(appConfigurationCaptor);

mockOrganizationsWithoutPermissions(appConfigurationCaptor, "org1");
ConfigCheckResult checkResult = configValidator.checkConfig();

assertSuccessfulAppConfig(checkResult);
assertThat(checkResult.installations())
.extracting(InstallationStatus::organization, InstallationStatus::autoProvisioning)
.containsExactly(tuple("org1", ConfigStatus.failed(MISSING_MEMBERS_PERMISSION)));
verifyAppConfiguration(appConfigurationCaptor.getValue());

}

@Test
public void checkConfig_whenInstallationSuspended_shouldReturnFailedInstallationAutoProvisioningCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);
mockGithubAppWithValidConfig(appConfigurationCaptor);

mockSuspendedOrganizations("org1");
ConfigCheckResult checkResult = configValidator.checkConfig();

assertSuccessfulAppConfig(checkResult);
assertThat(checkResult.installations())
.extracting(InstallationStatus::organization, InstallationStatus::autoProvisioning)
.containsExactly(tuple("org1", ConfigStatus.failed(SUSPENDED_INSTALLATION)));
verify(githubClient).getWhitelistedGithubAppInstallations(appConfigurationCaptor.capture());
verifyAppConfiguration(appConfigurationCaptor.getValue());
}

@Test
public void checkConfig_whenAllPermissionsAreCorrect_shouldReturnSuccessfulCheck() {
mockGithubConfiguration();
ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor = ArgumentCaptor.forClass(GithubAppConfiguration.class);
mockGithubAppWithValidConfig(appConfigurationCaptor);

mockOrganizations(appConfigurationCaptor, "org1", "org2");
ConfigCheckResult checkResult = configValidator.checkConfig();
assertSuccessfulAppConfig(checkResult);

assertThat(checkResult.installations())
.extracting(InstallationStatus::organization, InstallationStatus::autoProvisioning)
.containsExactlyInAnyOrder(
tuple("org1", SUCCESS_CHECK),
tuple("org2", ConfigStatus.failed(MISSING_MEMBERS_PERMISSION)));
verifyAppConfiguration(appConfigurationCaptor.getValue());

}

private void mockGithubConfiguration() {
when(gitHubSettings.appId()).thenReturn(APP_ID);
when(gitHubSettings.privateKey()).thenReturn(PRIVATE_KEY);
when(gitHubSettings.apiURLOrDefault()).thenReturn(URL);
}

private void verifyAppConfiguration(GithubAppConfiguration appConfiguration) {
assertThat(appConfiguration.getId()).isEqualTo(parseLong(APP_ID));
assertThat(appConfiguration.getPrivateKey()).isEqualTo(PRIVATE_KEY);
assertThat(appConfiguration.getApiEndpoint()).isEqualTo(URL);
}

private GsonApp mockGithubApp(ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor) {
GsonApp githubApp = mock(GsonApp.class);
when(githubClient.getApp(appConfigurationCaptor.capture())).thenReturn(githubApp);
return githubApp;
}

private GsonApp mockGithubAppWithValidConfig(ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor) {
GsonApp githubApp = mock(GsonApp.class);
when(githubClient.getApp(appConfigurationCaptor.capture())).thenReturn(githubApp);
when(githubApp.getPermissions()).thenReturn(new Permissions(null, "read", "read"));

return githubApp;
}

private static void assertSuccessfulAppConfig(ConfigCheckResult checkResult) {
assertThat(checkResult.application().jit()).isEqualTo(ConfigStatus.SUCCESS);
assertThat(checkResult.application().autoProvisioning()).isEqualTo(ConfigStatus.SUCCESS);
}

private void mockOrganizationsWithoutPermissions(ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor, String... organizations) {

List<GithubAppInstallation> installations = Arrays.stream(organizations).map(GithubProvisioningConfigValidatorTest::mockInstallation).toList();
when(githubClient.getWhitelistedGithubAppInstallations(appConfigurationCaptor.capture())).thenReturn(installations);

}

private void mockSuspendedOrganizations(String orgName) {
GithubAppInstallation installation = new GithubAppInstallation(null, orgName, null, true);
when(githubClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of(installation));
}

private static GithubAppInstallation mockInstallation(String org) {
GithubAppInstallation installation = mock(GithubAppInstallation.class);
when(installation.organizationName()).thenReturn(org);
when(installation.permissions()).thenReturn(mock(Permissions.class));
return installation;
}

private static GithubAppInstallation mockInstallationWithMembersPermission(String org) {
GithubAppInstallation installation = mockInstallation(org);
when(installation.permissions()).thenReturn(new Permissions(null, "read", "read"));
return installation;
}

private void mockOrganizations(ArgumentCaptor<GithubAppConfiguration> appConfigurationCaptor, String orgWithMembersPermission, String orgWithoutMembersPermission) {
List<GithubAppInstallation> installations = List.of(
mockInstallationWithMembersPermission(orgWithMembersPermission),
mockInstallation(orgWithoutMembersPermission));
when(githubClient.getWhitelistedGithubAppInstallations(appConfigurationCaptor.capture())).thenReturn(installations);
}

}

+ 107
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/config/CheckActionIT.java View File

@@ -0,0 +1,107 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.server.almintegration.ws.github.config;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.github.config.ConfigCheckResult;
import org.sonar.alm.client.github.config.ConfigCheckResult.ApplicationStatus;
import org.sonar.alm.client.github.config.GithubProvisioningConfigValidator;
import org.sonar.api.server.ws.WebService;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
import org.sonar.server.ws.WsActionTester;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.alm.client.github.config.ConfigCheckResult.ConfigStatus;
import static org.sonar.alm.client.github.config.ConfigCheckResult.InstallationStatus;
import static org.sonar.test.JsonAssert.assertJson;

public class CheckActionIT {

@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

private GithubProvisioningConfigValidator configValidator = mock(GithubProvisioningConfigValidator.class);

private final WsActionTester ws = new WsActionTester(new CheckAction(userSession, configValidator));

@Test
public void test_definition() {
WebService.Action def = ws.getDef();
assertThat(def.key()).isEqualTo("check");
assertThat(def.since()).isEqualTo("10.1");
assertThat(def.isInternal()).isTrue();
assertThat(def.isPost()).isTrue();
assertThat(def.params()).isEmpty();
assertThat(def.responseExample()).isNotNull();
assertThat(def.responseExample()).isEqualTo(getClass().getResource("check-example.json"));
}

@Test
public void check_whenUserIsAdmin_shouldReturnCheckResult() throws IOException {
userSession.logIn().setSystemAdministrator();
ConfigCheckResult result = new ConfigCheckResult(
new ApplicationStatus(
ConfigStatus.SUCCESS,
ConfigStatus.failed("App validation failed")),
List.of(
new InstallationStatus(
"org1",
ConfigStatus.SUCCESS,
ConfigStatus.SUCCESS
),
new InstallationStatus(
"org2",
ConfigStatus.SUCCESS,
ConfigStatus.failed("Organization validation failed.")
)
));

when(configValidator.checkConfig()).thenReturn(result);
TestResponse response = ws.newRequest().execute();

assertThat(response.getStatus()).isEqualTo(200);
assertJson(response.getInput()).isSimilarTo(readResponse("check-example.json"));
}

@Test
public void check_whenNotAnAdmin_shouldThrow() {
userSession.logIn("not-an-admin");

TestRequest testRequest = ws.newRequest();
assertThatThrownBy(testRequest::execute)
.hasMessage("Insufficient privileges")
.isInstanceOf(ForbiddenException.class);
}

private String readResponse(String file) throws IOException {
return IOUtils.toString(getClass().getResource(file), StandardCharsets.UTF_8);
}
}

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

@@ -32,6 +32,7 @@ import org.sonar.server.almintegration.ws.github.GetGithubClientIdAction;
import org.sonar.server.almintegration.ws.github.ImportGithubProjectAction;
import org.sonar.server.almintegration.ws.github.ListGithubOrganizationsAction;
import org.sonar.server.almintegration.ws.github.ListGithubRepositoriesAction;
import org.sonar.server.almintegration.ws.github.config.CheckAction;
import org.sonar.server.almintegration.ws.gitlab.ImportGitLabProjectAction;
import org.sonar.server.almintegration.ws.gitlab.SearchGitlabReposAction;

@@ -40,6 +41,7 @@ public class AlmIntegrationsWSModule extends Module {
protected void configureModule() {
add(
CheckPatAction.class,
CheckAction.class,
SetPatAction.class,
ImportBitbucketServerProjectAction.class,
ImportBitbucketCloudRepoAction.class,

+ 25
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/GithubProvisioningAction.java View File

@@ -0,0 +1,25 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.server.almintegration.ws.github;

import org.sonar.server.ws.WsAction;

public interface GithubProvisioningAction extends WsAction {
}

+ 46
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/GithubProvisioningWs.java View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.server.almintegration.ws.github;

import java.util.Set;
import org.sonar.api.server.ServerSide;
import org.sonar.api.server.ws.WebService;

@ServerSide
public class GithubProvisioningWs implements WebService {

public static final String API_ENDPOINT = "api/github_provisioning";

private final Set<GithubProvisioningAction> actions;

public GithubProvisioningWs(Set<GithubProvisioningAction> actions) {
this.actions = actions;
}

@Override
public void define(Context context) {
NewController controller = context.createController(API_ENDPOINT)
.setDescription("Manage GitHub provisioning.")
.setSince("10.1");

actions.forEach(action -> action.define(controller));
controller.done();
}
}

+ 69
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/config/CheckAction.java View File

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.server.almintegration.ws.github.config;

import com.google.gson.GsonBuilder;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import org.sonar.alm.client.github.config.ConfigCheckResult;
import org.sonar.alm.client.github.config.GithubProvisioningConfigValidator;
import org.sonar.api.server.ServerSide;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.server.almintegration.ws.github.GithubProvisioningAction;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.MediaTypes;

@ServerSide
public class CheckAction implements GithubProvisioningAction {

private final UserSession userSession;
private final GithubProvisioningConfigValidator githubProvisioningConfigValidator;

public CheckAction(UserSession userSession, GithubProvisioningConfigValidator githubProvisioningConfigValidator) {
this.userSession = userSession;
this.githubProvisioningConfigValidator = githubProvisioningConfigValidator;
}

@Override
public void define(WebService.NewController controller) {
controller
.createAction("check")
.setPost(true)
.setDescription("Validate Github provisioning configuration.")
.setHandler(this)
.setInternal(true)
.setResponseExample(getClass().getResource("check-example.json"))
.setSince("10.1");
}

@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkIsSystemAdministrator();

ConfigCheckResult result = githubProvisioningConfigValidator.checkConfig();

response.stream().setStatus(HttpServletResponse.SC_OK);
response.stream().setMediaType(MediaTypes.JSON);
response.stream().output().write(new GsonBuilder().create().toJson(result).getBytes(StandardCharsets.UTF_8));
response.stream().output().flush();
}
}

+ 32
- 0
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/github/config/check-example.json View File

@@ -0,0 +1,32 @@
{
"application": {
"jit": {
"status": "SUCCESS"
},
"autoProvisioning": {
"status": "FAILED",
"errorMessage": "App validation failed"
}
},
"installations": [
{
"organization": "org1",
"jit": {
"status": "SUCCESS"
},
"autoProvisioning": {
"status": "SUCCESS"
}
},
{
"organization": "org2",
"jit": {
"status": "SUCCESS"
},
"autoProvisioning": {
"status": "FAILED",
"errorMessage": "Organization validation failed."
}
}
]
}

+ 49
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/github/GithubProvisioningWsTest.java View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.server.almintegration.ws.github;

import java.util.Set;
import org.junit.Test;
import org.sonar.api.server.ws.WebService;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class GithubProvisioningWsTest {
@Test
public void define_createsOneController() {
WebService.Context context = mock(WebService.Context.class);
GithubProvisioningAction action = mock(GithubProvisioningAction.class);
WebService.NewController controller = mock(WebService.NewController.class);
GithubProvisioningWs scimWs = new GithubProvisioningWs(Set.of(action));

when(context.createController("api/github_provisioning")).thenReturn(controller);
when(controller.setDescription(any())).thenReturn(controller);
when(controller.setSince(any())).thenReturn(controller);

scimWs.define(context);

verify(context, only()).createController("api/github_provisioning");
verify(action, only()).define(any());
}
}

+ 4
- 0
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -30,6 +30,7 @@ import org.sonar.alm.client.bitbucketserver.BitbucketServerSettingsValidator;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.GithubApplicationHttpClientImpl;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.config.GithubProvisioningConfigValidator;
import org.sonar.alm.client.github.security.GithubAppSecurityImpl;
import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
@@ -59,6 +60,7 @@ import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule;
import org.sonar.server.almintegration.ws.CredentialsEncoderHelper;
import org.sonar.server.almintegration.ws.ImportHelper;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.almintegration.ws.github.GithubProvisioningWs;
import org.sonar.server.almsettings.MultipleAlmFeature;
import org.sonar.server.almsettings.ws.AlmSettingsWsModule;
import org.sonar.server.authentication.AuthenticationModule;
@@ -543,6 +545,8 @@ public class PlatformLevel4 extends PlatformLevel {
GithubAppSecurityImpl.class,
GithubApplicationClientImpl.class,
GithubApplicationHttpClientImpl.class,
GithubProvisioningConfigValidator.class,
GithubProvisioningWs.class,
BitbucketCloudRestClientConfiguration.class,
BitbucketServerRestClient.class,
GitlabHttpClient.class,

Loading…
Cancel
Save