@@ -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') | |||
@@ -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. | |||
*/ |
@@ -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 { |
@@ -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) { | |||
} | |||
} |
@@ -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) {} |
@@ -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; | |||
} | |||
} |
@@ -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"; |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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, |
@@ -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 { | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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." | |||
} | |||
} | |||
] | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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, |