aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-alm-client
diff options
context:
space:
mode:
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>2023-06-02 14:14:30 +0200
committersonartech <sonartech@sonarsource.com>2023-06-05 20:02:48 +0000
commitc1966338ee963dbd6e6f636635f390cfeb334af5 (patch)
tree0db158f7b2229db3cd33388f82b2664a5329b729 /server/sonar-alm-client
parentb2e7f33ced83a025961195ba5065986e94aae9f0 (diff)
downloadsonarqube-c1966338ee963dbd6e6f636635f390cfeb334af5.tar.gz
sonarqube-c1966338ee963dbd6e6f636635f390cfeb334af5.zip
SONAR-19346 GitHub config check available in Community Edition
Diffstat (limited to 'server/sonar-alm-client')
-rw-r--r--server/sonar-alm-client/build.gradle1
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java9
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java76
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java51
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java24
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java167
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java139
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java294
8 files changed, 759 insertions, 2 deletions
diff --git a/server/sonar-alm-client/build.gradle b/server/sonar-alm-client/build.gradle
index a15661a6cba..fa06b389318 100644
--- a/server/sonar-alm-client/build.gradle
+++ b/server/sonar-alm-client/build.gradle
@@ -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')
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
index 7446d304ec6..11848ba0f4a 100644
--- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
@@ -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,12 +44,20 @@ 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.
*/
Repositories listRepositories(String appUrl, AccessToken accessToken, String organization, @Nullable String query, int page, int pageSize);
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
index eb7a4718a34..4b771efee8e 100644
--- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
@@ -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) {
@@ -161,6 +169,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);
String searchQuery = "fork:true+org:" + organization;
@@ -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 {
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java
new file mode 100644
index 00000000000..1af06e2d4d1
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/ConfigCheckResult.java
@@ -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) {
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java
new file mode 100644
index 00000000000..5f59f48c617
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubAppInstallation.java
@@ -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) {}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java
new file mode 100644
index 00000000000..3e6bb8f001f
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidator.java
@@ -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;
+ }
+
+}
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
index fbe8aa95ca0..2c5e4a4726b 100644
--- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
@@ -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[][] {
@@ -393,6 +454,82 @@ public class GithubApplicationClientImplTest {
}
@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";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java
new file mode 100644
index 00000000000..e743102c734
--- /dev/null
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/config/GithubProvisioningConfigValidatorTest.java
@@ -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);
+ }
+
+}