aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>2024-06-11 17:44:45 +0200
committersonartech <sonartech@sonarsource.com>2024-06-17 20:02:35 +0000
commit078306d53ad53ba38d5d4b06e6e8958a0c2c6595 (patch)
treecd0ac74f560aac0e2a3720b096239d7519f856c0 /server
parent0bdfddeed0bf06255f61c6b59dcfc6d132598e14 (diff)
downloadsonarqube-078306d53ad53ba38d5d4b06e6e8958a0c2c6595.tar.gz
sonarqube-078306d53ad53ba38d5d4b06e6e8958a0c2c6595.zip
SONAR-22365 Fix SSF-571
Diffstat (limited to 'server')
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java24
-rw-r--r--server/sonar-auth-github/src/it/java/org/sonar/auth/github/GitHubSettingsIT.java22
-rw-r--r--server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java82
-rw-r--r--server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java28
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/AlmSettingDto.java8
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/config/GithubConfigurationServiceIT.java537
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfiguration.java41
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfigurationService.java352
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/UpdateGithubConfigurationRequest.java141
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/DefaultGithubConfigurationController.java164
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java97
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationCreateRestRequest.java97
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationUpdateRestRequest.java160
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/GithubConfigurationResource.java61
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/GithubConfigurationSearchRestResponse.java27
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java6
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/ProvisioningType.java (renamed from server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/ProvisioningType.java)2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java10
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/config/DefaultGithubConfigurationControllerTest.java499
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java3
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java23
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SetAction.java5
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java2
32 files changed, 2430 insertions, 83 deletions
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java
index f6758823fb9..780c184612d 100644
--- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubGlobalSettingsValidator.java
@@ -20,6 +20,8 @@
package org.sonar.alm.client.github;
import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.api.server.ServerSide;
@@ -40,25 +42,35 @@ public class GithubGlobalSettingsValidator {
this.githubApplicationClient = githubApplicationClient;
}
- public GithubAppConfiguration validate(AlmSettingDto settings) {
+ public GithubAppConfiguration validate(AlmSettingDto almSettingDto) {
+ return validate(almSettingDto.getAppId(), almSettingDto.getClientId(), almSettingDto.getClientSecret(), almSettingDto.getPrivateKey(), almSettingDto.getUrl());
+ }
+
+ public GithubAppConfiguration validate(@Nullable String applicationId, @Nullable String clientId, String clientSecret, String privateKey, @Nullable String url) {
long appId;
try {
- appId = Long.parseLong(Optional.ofNullable(settings.getAppId()).orElseThrow(() -> new IllegalArgumentException("Missing appId")));
+ appId = Long.parseLong(Optional.ofNullable(applicationId).orElseThrow(() -> new IllegalArgumentException("Missing appId")));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid appId; " + e.getMessage());
}
- if (isBlank(settings.getClientId())) {
+ if (isBlank(clientId)) {
throw new IllegalArgumentException("Missing Client Id");
}
- if (isBlank(settings.getDecryptedClientSecret(encryption))) {
+ if (isBlank(getDecryptedSettingValue(clientSecret))) {
throw new IllegalArgumentException("Missing Client Secret");
}
- GithubAppConfiguration configuration = new GithubAppConfiguration(appId, settings.getDecryptedPrivateKey(encryption),
- settings.getUrl());
+ GithubAppConfiguration configuration = new GithubAppConfiguration(appId, getDecryptedSettingValue(privateKey), url);
githubApplicationClient.checkApiEndpoint(configuration);
githubApplicationClient.checkAppPermissions(configuration);
return configuration;
}
+
+ private String getDecryptedSettingValue(String setting) {
+ if (StringUtils.isNotEmpty(setting) && encryption.isEncrypted(setting)) {
+ return encryption.decrypt(setting);
+ }
+ return setting;
+ }
}
diff --git a/server/sonar-auth-github/src/it/java/org/sonar/auth/github/GitHubSettingsIT.java b/server/sonar-auth-github/src/it/java/org/sonar/auth/github/GitHubSettingsIT.java
index 89f8bf4dd16..eab5384f3aa 100644
--- a/server/sonar-auth-github/src/it/java/org/sonar/auth/github/GitHubSettingsIT.java
+++ b/server/sonar-auth-github/src/it/java/org/sonar/auth/github/GitHubSettingsIT.java
@@ -37,8 +37,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.sonar.auth.github.GitHubSettings.PROVISION_VISIBILITY;
-import static org.sonar.auth.github.GitHubSettings.USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PROVISION_PROJECT_VISIBILITY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE;
public class GitHubSettingsIT {
@Rule
@@ -80,27 +80,27 @@ public class GitHubSettingsIT {
@Test
public void isProvisioningEnabled_returnsFalseByDefault() {
enableGithubAuthentication();
- when(internalProperties.read(GitHubSettings.PROVISIONING)).thenReturn(Optional.empty());
+ when(internalProperties.read(GitHubSettings.GITHUB_PROVISIONING)).thenReturn(Optional.empty());
assertThat(underTest.isProvisioningEnabled()).isFalse();
}
@Test
public void isProvisioningEnabled_ifProvisioningEnabledButGithubAuthNotSet_returnsFalse() {
enableGithubAuthentication();
- when(internalProperties.read(GitHubSettings.PROVISIONING)).thenReturn(Optional.of(Boolean.FALSE.toString()));
+ when(internalProperties.read(GitHubSettings.GITHUB_PROVISIONING)).thenReturn(Optional.of(Boolean.FALSE.toString()));
assertThat(underTest.isProvisioningEnabled()).isFalse();
}
@Test
public void isProvisioningEnabled_ifProvisioningEnabledButGithubAuthDisabled_returnsFalse() {
- when(internalProperties.read(GitHubSettings.PROVISIONING)).thenReturn(Optional.of(Boolean.TRUE.toString()));
+ when(internalProperties.read(GitHubSettings.GITHUB_PROVISIONING)).thenReturn(Optional.of(Boolean.TRUE.toString()));
assertThat(underTest.isProvisioningEnabled()).isFalse();
}
@Test
public void isProvisioningEnabled_ifProvisioningEnabledAndGithubAuthEnabled_returnsTrue() {
enableGithubAuthenticationWithGithubApp();
- when(internalProperties.read(GitHubSettings.PROVISIONING)).thenReturn(Optional.of(Boolean.TRUE.toString()));
+ when(internalProperties.read(GitHubSettings.GITHUB_PROVISIONING)).thenReturn(Optional.of(Boolean.TRUE.toString()));
assertThat(underTest.isProvisioningEnabled()).isTrue();
}
@@ -111,7 +111,7 @@ public class GitHubSettingsIT {
@Test
public void isUserConsentRequiredAfterUpgrade_returnsTrueIfPropertyPresent() {
- settings.setProperty(USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE, "");
+ settings.setProperty(GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE, "");
assertThat(underTest.isUserConsentRequiredAfterUpgrade()).isTrue();
}
@@ -122,13 +122,13 @@ public class GitHubSettingsIT {
@Test
public void isProjectVisibilitySynchronizationActivated_whenPropertyIsSetToFalse_returnsFalse() {
- settings.setProperty(PROVISION_VISIBILITY, "false");
+ settings.setProperty(GITHUB_PROVISION_PROJECT_VISIBILITY, "false");
assertThat(underTest.isProjectVisibilitySynchronizationActivated()).isFalse();
}
@Test
public void isProjectVisibilitySynchronizationActivated_whenPropertyIsSetToTrue_returnsTrue() {
- settings.setProperty(PROVISION_VISIBILITY, "true");
+ settings.setProperty(GITHUB_PROVISION_PROJECT_VISIBILITY, "true");
assertThat(underTest.isProjectVisibilitySynchronizationActivated()).isTrue();
}
@@ -166,7 +166,7 @@ public class GitHubSettingsIT {
public void setProvisioning_whenPassedTrue_delegatesToInternalPropertiesWrite() {
enableGithubAuthenticationWithGithubApp();
underTest.setProvisioning(true);
- verify(internalProperties).write(GitHubSettings.PROVISIONING, Boolean.TRUE.toString());
+ verify(internalProperties).write(GitHubSettings.GITHUB_PROVISIONING, Boolean.TRUE.toString());
}
@Test
@@ -176,7 +176,7 @@ public class GitHubSettingsIT {
underTest.setProvisioning(false);
- verify(internalProperties).write(GitHubSettings.PROVISIONING, Boolean.FALSE.toString());
+ verify(internalProperties).write(GitHubSettings.GITHUB_PROVISIONING, Boolean.FALSE.toString());
assertThat(db.getDbClient().externalGroupDao().selectByIdentityProvider(db.getSession(), GitHubIdentityProvider.KEY)).isEmpty();
assertThat(db.getDbClient().githubOrganizationGroupDao().findAll(db.getSession())).isEmpty();
}
diff --git a/server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java b/server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java
index 90e49d11556..57696283ba3 100644
--- a/server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java
+++ b/server/sonar-auth-github/src/main/java/org/sonar/auth/github/GitHubSettings.java
@@ -19,7 +19,6 @@
*/
package org.sonar.auth.github;
-import com.google.common.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@@ -48,24 +47,21 @@ import static org.sonar.api.utils.Preconditions.checkState;
@ComputeEngineSide
public class GitHubSettings implements DevOpsPlatformSettings {
- public static final String CLIENT_ID = "sonar.auth.github.clientId.secured";
- public static final String CLIENT_SECRET = "sonar.auth.github.clientSecret.secured";
- public static final String APP_ID = "sonar.auth.github.appId";
- public static final String PRIVATE_KEY = "sonar.auth.github.privateKey.secured";
- public static final String ENABLED = "sonar.auth.github.enabled";
- public static final String ALLOW_USERS_TO_SIGN_UP = "sonar.auth.github.allowUsersToSignUp";
- public static final String GROUPS_SYNC = "sonar.auth.github.groupsSync";
- public static final String API_URL = "sonar.auth.github.apiUrl";
+ public static final String GITHUB_CLIENT_ID = "sonar.auth.github.clientId.secured";
+ public static final String GITHUB_CLIENT_SECRET = "sonar.auth.github.clientSecret.secured";
+ public static final String GITHUB_APP_ID = "sonar.auth.github.appId";
+ public static final String GITHUB_PRIVATE_KEY = "sonar.auth.github.privateKey.secured";
+ public static final String GITHUB_ENABLED = "sonar.auth.github.enabled";
+ public static final String GITHUB_ALLOW_USERS_TO_SIGN_UP = "sonar.auth.github.allowUsersToSignUp";
+ public static final String GITHUB_GROUPS_SYNC = "sonar.auth.github.groupsSync";
+ public static final String GITHUB_API_URL = "sonar.auth.github.apiUrl";
public static final String DEFAULT_API_URL = "https://api.github.com/";
- public static final String WEB_URL = "sonar.auth.github.webUrl";
+ public static final String GITHUB_WEB_URL = "sonar.auth.github.webUrl";
public static final String DEFAULT_WEB_URL = "https://github.com/";
- public static final String ORGANIZATIONS = "sonar.auth.github.organizations";
- @VisibleForTesting
- static final String PROVISIONING = "provisioning.github.enabled";
- @VisibleForTesting
- static final String PROVISION_VISIBILITY = "provisioning.github.project.visibility.enabled";
- @VisibleForTesting
- static final String USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE = "sonar.auth.github.userConsentForPermissionProvisioningRequired";
+ public static final String GITHUB_ORGANIZATIONS = "sonar.auth.github.organizations";
+ public static final String GITHUB_PROVISIONING = "provisioning.github.enabled";
+ public static final String GITHUB_PROVISION_PROJECT_VISIBILITY = "provisioning.github.project.visibility.enabled";
+ public static final String GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE = "sonar.auth.github.userConsentForPermissionProvisioningRequired";
private static final String CATEGORY = "authentication";
private static final String SUBCATEGORY = "github";
@@ -82,49 +78,49 @@ public class GitHubSettings implements DevOpsPlatformSettings {
}
public String clientId() {
- return configuration.get(CLIENT_ID).orElse("");
+ return configuration.get(GITHUB_CLIENT_ID).orElse("");
}
public String clientSecret() {
- return configuration.get(CLIENT_SECRET).orElse("");
+ return configuration.get(GITHUB_CLIENT_SECRET).orElse("");
}
public String appId() {
- return configuration.get(APP_ID).orElse("");
+ return configuration.get(GITHUB_APP_ID).orElse("");
}
public String privateKey() {
- return configuration.get(PRIVATE_KEY).orElse("");
+ return configuration.get(GITHUB_PRIVATE_KEY).orElse("");
}
public boolean isEnabled() {
- return configuration.getBoolean(ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty();
+ return configuration.getBoolean(GITHUB_ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty();
}
public boolean allowUsersToSignUp() {
- return configuration.getBoolean(ALLOW_USERS_TO_SIGN_UP).orElse(false);
+ return configuration.getBoolean(GITHUB_ALLOW_USERS_TO_SIGN_UP).orElse(false);
}
public boolean syncGroups() {
- return configuration.getBoolean(GROUPS_SYNC).orElse(false);
+ return configuration.getBoolean(GITHUB_GROUPS_SYNC).orElse(false);
}
@CheckForNull
String webURL() {
- return urlWithEndingSlash(configuration.get(WEB_URL).orElse(""));
+ return urlWithEndingSlash(configuration.get(GITHUB_WEB_URL).orElse(""));
}
@CheckForNull
public String apiURL() {
- return urlWithEndingSlash(configuration.get(API_URL).orElse(""));
+ return urlWithEndingSlash(configuration.get(GITHUB_API_URL).orElse(""));
}
public String apiURLOrDefault() {
- return configuration.get(API_URL).map(GitHubSettings::urlWithEndingSlash).orElse(DEFAULT_API_URL);
+ return configuration.get(GITHUB_API_URL).map(GitHubSettings::urlWithEndingSlash).orElse(DEFAULT_API_URL);
}
public Set<String> getOrganizations() {
- return Set.of(configuration.getStringArray(ORGANIZATIONS));
+ return Set.of(configuration.getStringArray(GITHUB_ORGANIZATIONS));
}
@CheckForNull
@@ -141,7 +137,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
} else {
removeExternalGroupsForGithub();
}
- internalProperties.write(PROVISIONING, String.valueOf(enableProvisioning));
+ internalProperties.write(GITHUB_PROVISIONING, String.valueOf(enableProvisioning));
}
private void removeExternalGroupsForGithub() {
@@ -169,22 +165,22 @@ public class GitHubSettings implements DevOpsPlatformSettings {
@Override
public boolean isProvisioningEnabled() {
- return isEnabled() && internalProperties.read(PROVISIONING).map(Boolean::parseBoolean).orElse(false);
+ return isEnabled() && internalProperties.read(GITHUB_PROVISIONING).map(Boolean::parseBoolean).orElse(false);
}
public boolean isUserConsentRequiredAfterUpgrade() {
- return configuration.get(USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE).isPresent();
+ return configuration.get(GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE).isPresent();
}
@Override
public boolean isProjectVisibilitySynchronizationActivated() {
- return configuration.getBoolean(PROVISION_VISIBILITY).orElse(true);
+ return configuration.getBoolean(GITHUB_PROVISION_PROJECT_VISIBILITY).orElse(true);
}
public static List<PropertyDefinition> definitions() {
int index = 1;
return Arrays.asList(
- PropertyDefinition.builder(ENABLED)
+ PropertyDefinition.builder(GITHUB_ENABLED)
.name("Enabled")
.description("Enable GitHub users to login. Value is ignored if client ID and secret are not defined.")
.category(CATEGORY)
@@ -193,14 +189,14 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.defaultValue(valueOf(false))
.index(index++)
.build(),
- PropertyDefinition.builder(CLIENT_ID)
+ PropertyDefinition.builder(GITHUB_CLIENT_ID)
.name("Client ID")
.description("Client ID provided by GitHub when registering the application.")
.category(CATEGORY)
.subCategory(SUBCATEGORY)
.index(index++)
.build(),
- PropertyDefinition.builder(CLIENT_SECRET)
+ PropertyDefinition.builder(GITHUB_CLIENT_SECRET)
.name("Client Secret")
.description("Client password provided by GitHub when registering the application.")
.category(CATEGORY)
@@ -208,7 +204,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.type(PASSWORD)
.index(index++)
.build(),
- PropertyDefinition.builder(APP_ID)
+ PropertyDefinition.builder(GITHUB_APP_ID)
.name("App ID")
.description("The App ID is found on your GitHub App's page on GitHub at Settings > Developer Settings > GitHub Apps.")
.category(CATEGORY)
@@ -216,7 +212,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.type(STRING)
.index(index++)
.build(),
- PropertyDefinition.builder(PRIVATE_KEY)
+ PropertyDefinition.builder(GITHUB_PRIVATE_KEY)
.name("Private Key")
.description("""
Your GitHub App's private key. You can generate a .pem file from your GitHub App's page under Private keys.
@@ -226,7 +222,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.type(PropertyType.TEXT)
.index(index++)
.build(),
- PropertyDefinition.builder(ALLOW_USERS_TO_SIGN_UP)
+ PropertyDefinition.builder(GITHUB_ALLOW_USERS_TO_SIGN_UP)
.name("Allow users to sign up")
.description("Allow new users to authenticate. When set to disabled, only existing users will be able to authenticate to the server.")
.category(CATEGORY)
@@ -235,7 +231,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.defaultValue(valueOf(true))
.index(index++)
.build(),
- PropertyDefinition.builder(GROUPS_SYNC)
+ PropertyDefinition.builder(GITHUB_GROUPS_SYNC)
.name("Synchronize teams as groups")
.description("Synchronize GitHub team with SonarQube group memberships when users log in to SonarQube."
+ " For each GitHub team they belong to, users will be associated to a group of the same name if it exists in SonarQube.")
@@ -245,7 +241,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.defaultValue(valueOf(false))
.index(index++)
.build(),
- PropertyDefinition.builder(API_URL)
+ PropertyDefinition.builder(GITHUB_API_URL)
.name("The API url for a GitHub instance.")
.description(String.format("The API url for a GitHub instance. %s for Github.com, https://github.company.com/api/v3/ when using Github Enterprise", DEFAULT_API_URL))
.category(CATEGORY)
@@ -254,7 +250,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.defaultValue(DEFAULT_API_URL)
.index(index++)
.build(),
- PropertyDefinition.builder(WEB_URL)
+ PropertyDefinition.builder(GITHUB_WEB_URL)
.name("The WEB url for a GitHub instance.")
.description(String.format("The WEB url for a GitHub instance. %s for Github.com, https://github.company.com/ when using GitHub Enterprise.", DEFAULT_WEB_URL))
.category(CATEGORY)
@@ -263,7 +259,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.defaultValue(DEFAULT_WEB_URL)
.index(index++)
.build(),
- PropertyDefinition.builder(ORGANIZATIONS)
+ PropertyDefinition.builder(GITHUB_ORGANIZATIONS)
.name("Organizations")
.description("Only members of these organizations will be able to authenticate to the server. "
+ "⚠ if not set, users from any organization where the GitHub App is installed will be able to login to this SonarQube instance.")
@@ -272,7 +268,7 @@ public class GitHubSettings implements DevOpsPlatformSettings {
.subCategory(SUBCATEGORY)
.index(index)
.build(),
- PropertyDefinition.builder(PROVISION_VISIBILITY)
+ PropertyDefinition.builder(GITHUB_PROVISION_PROJECT_VISIBILITY)
.name("Provision project visibility")
.description("Change project visibility based on GitHub repository visibility. If disabled, every provisioned project will be private in SonarQube and visible only"
+ " to users with explicit GitHub permissions for the corresponding repository. Changes take effect at the next synchronization.")
diff --git a/server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java b/server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
index 7e7aab267f2..774daaca488 100644
--- a/server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
+++ b/server/sonar-auth-github/src/test/java/org/sonar/auth/github/GitHubIdentityProviderTest.java
@@ -43,12 +43,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.CallbackContext;
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.InitContext;
-import static org.sonar.auth.github.GitHubSettings.APP_ID;
-import static org.sonar.auth.github.GitHubSettings.CLIENT_ID;
-import static org.sonar.auth.github.GitHubSettings.CLIENT_SECRET;
-import static org.sonar.auth.github.GitHubSettings.ENABLED;
-import static org.sonar.auth.github.GitHubSettings.ORGANIZATIONS;
-import static org.sonar.auth.github.GitHubSettings.PRIVATE_KEY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_APP_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_SECRET;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ENABLED;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ORGANIZATIONS;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PRIVATE_KEY;
public class GitHubIdentityProviderTest {
@@ -180,7 +180,7 @@ public class GitHubIdentityProviderTest {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserBelongingToOrganization(userIdentity);
- settings.setProperty(ORGANIZATIONS, "organization1,organization2");
+ settings.setProperty(GITHUB_ORGANIZATIONS, "organization1,organization2");
underTest.callback(context);
verify(context).authenticate(userIdentity);
@@ -192,7 +192,7 @@ public class GitHubIdentityProviderTest {
UserIdentity userIdentity = mock(UserIdentity.class);
CallbackContext context = mockUserNotBelongingToOrganization(userIdentity);
- settings.setProperty(ORGANIZATIONS, "organization1,organization2");
+ settings.setProperty(GITHUB_ORGANIZATIONS, "organization1,organization2");
assertThatThrownBy(() -> underTest.callback(context))
.isInstanceOf(UnauthorizedException.class)
@@ -280,13 +280,13 @@ public class GitHubIdentityProviderTest {
private void setSettings(boolean enabled) {
if (enabled) {
- settings.setProperty(CLIENT_ID, "id");
- settings.setProperty(CLIENT_SECRET, "secret");
- settings.setProperty(ENABLED, true);
- settings.setProperty(APP_ID, "1");
- settings.setProperty(PRIVATE_KEY, "private");
+ settings.setProperty(GITHUB_CLIENT_ID, "id");
+ settings.setProperty(GITHUB_CLIENT_SECRET, "secret");
+ settings.setProperty(GITHUB_ENABLED, true);
+ settings.setProperty(GITHUB_APP_ID, "1");
+ settings.setProperty(GITHUB_PRIVATE_KEY, "private");
} else {
- settings.setProperty(ENABLED, false);
+ settings.setProperty(GITHUB_ENABLED, false);
}
}
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/AlmSettingDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/AlmSettingDto.java
index 16356025ad1..2186e9b0ebe 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/AlmSettingDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/AlmSettingDto.java
@@ -160,6 +160,10 @@ public class AlmSettingDto {
return privateKey;
}
+ public String getPrivateKey() {
+ return privateKey;
+ }
+
public AlmSettingDto setPrivateKey(@Nullable String privateKey) {
this.privateKey = privateKey;
return this;
@@ -196,6 +200,10 @@ public class AlmSettingDto {
return clientSecret;
}
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
public AlmSettingDto setClientSecret(@Nullable String clientSecret) {
this.clientSecret = clientSecret;
return this;
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/config/GithubConfigurationServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/config/GithubConfigurationServiceIT.java
new file mode 100644
index 00000000000..7072e7712f7
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/config/GithubConfigurationServiceIT.java
@@ -0,0 +1,537 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.github.config;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.auth.github.GitHubIdentityProvider;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.provisioning.GithubOrganizationGroupDto;
+import org.sonar.db.user.ExternalGroupDto;
+import org.sonar.server.common.gitlab.config.ProvisioningType;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.setting.ThreadLocalSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ALLOW_USERS_TO_SIGN_UP;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_API_URL;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_APP_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_SECRET;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ENABLED;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_GROUPS_SYNC;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ORGANIZATIONS;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PRIVATE_KEY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PROVISIONING;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PROVISION_PROJECT_VISIBILITY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_WEB_URL;
+import static org.sonar.server.common.NonNullUpdatedValue.withValueOrThrow;
+import static org.sonar.server.common.github.config.GithubConfigurationService.UNIQUE_GITHUB_CONFIGURATION_ID;
+import static org.sonar.server.common.github.config.UpdateGithubConfigurationRequest.builder;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GithubConfigurationServiceIT {
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ @Mock
+ private ManagedInstanceService managedInstanceService;
+
+ @Mock
+ private GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+
+ @Mock
+ private ThreadLocalSettings threadLocalSettings;
+
+ private GithubConfigurationService githubConfigurationService;
+
+ @Before
+ public void setUp() {
+ when(managedInstanceService.getProviderName()).thenReturn("github");
+ githubConfigurationService = new GithubConfigurationService(
+ dbTester.getDbClient(),
+ managedInstanceService,
+ githubGlobalSettingsValidator,
+ threadLocalSettings);
+ }
+
+ @Test
+ public void getConfiguration_whenIdIsNotGithubConfiguration_throwsException() {
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> githubConfigurationService.getConfiguration("not-github-configuration"))
+ .withMessage("GitHub configuration with id not-github-configuration not found");
+
+ assertThat(githubConfigurationService.findConfigurations()).isEmpty();
+ }
+
+ @Test
+ public void getConfiguration_whenNoConfiguration_throwsNotFoundException() {
+ assertThatThrownBy(() -> githubConfigurationService.getConfiguration("github-configuration"))
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("GitHub configuration doesn't exist.");
+
+ assertThat(githubConfigurationService.findConfigurations()).isEmpty();
+ }
+
+ @Test
+ public void getConfiguration_whenConfigurationSet_returnsConfig() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(AUTO_PROVISIONING));
+
+ GithubConfiguration configuration = githubConfigurationService.getConfiguration("github-configuration");
+
+ assertConfigurationFields(configuration);
+
+ assertThat(githubConfigurationService.findConfigurations()).contains(configuration);
+ }
+
+ @Test
+ public void getConfiguration_whenConfigurationSetAndEmpty_returnsConfig() {
+ dbTester.properties().insertProperty(GITHUB_ENABLED, "true", null);
+ dbTester.properties().insertProperty(GITHUB_ORGANIZATIONS, "", null);
+
+ GithubConfiguration configuration = githubConfigurationService.getConfiguration("github-configuration");
+
+ assertThat(configuration.id()).isEqualTo("github-configuration");
+ assertThat(configuration.enabled()).isTrue();
+ assertThat(configuration.clientId()).isEmpty();
+ assertThat(configuration.clientSecret()).isEmpty();
+ assertThat(configuration.applicationId()).isEmpty();
+ assertThat(configuration.privateKey()).isEmpty();
+ assertThat(configuration.synchronizeGroups()).isFalse();
+ assertThat(configuration.apiUrl()).isEmpty();
+ assertThat(configuration.webUrl()).isEmpty();
+ assertThat(configuration.allowedOrganizations()).isEmpty();
+ assertThat(configuration.provisioningType()).isEqualTo(JIT);
+ assertThat(configuration.allowUsersToSignUp()).isFalse();
+ assertThat(configuration.provisionProjectVisibility()).isFalse();
+ assertThat(configuration.userConsentRequiredAfterUpgrade()).isFalse();
+ }
+
+ @Test
+ public void updateConfiguration_whenIdIsNotGithubConfiguration_throwsException() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(AUTO_PROVISIONING));
+ UpdateGithubConfigurationRequest updateGithubConfigurationRequest = builder().githubConfigurationId("not-github-configuration").build();
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> githubConfigurationService.updateConfiguration(updateGithubConfigurationRequest))
+ .withMessage("GitHub configuration with id not-github-configuration not found");
+ }
+
+ @Test
+ public void updateConfiguration_whenConfigurationDoesntExist_throwsException() {
+ UpdateGithubConfigurationRequest updateGithubConfigurationRequest = builder().githubConfigurationId("github-configuration").build();
+ assertThatExceptionOfType(NotFoundException.class)
+ .isThrownBy(() -> githubConfigurationService.updateConfiguration(updateGithubConfigurationRequest))
+ .withMessage("GitHub configuration doesn't exist.");
+ }
+
+ @Test
+ public void updateConfiguration_whenAllUpdateFieldDefined_updatesEverything() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(JIT));
+
+ UpdateGithubConfigurationRequest updateRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .enabled(withValueOrThrow(true))
+ .clientId(withValueOrThrow("clientId"))
+ .clientSecret(withValueOrThrow("clientSecret"))
+ .applicationId(withValueOrThrow("applicationId"))
+ .privateKey(withValueOrThrow("privateKey"))
+ .synchronizeGroups(withValueOrThrow(true))
+ .apiUrl(withValueOrThrow("apiUrl"))
+ .webUrl(withValueOrThrow("webUrl"))
+ .allowedOrganizations(withValueOrThrow(new LinkedHashSet<>(List.of("org1", "org2", "org3"))))
+ .provisioningType(withValueOrThrow(AUTO_PROVISIONING))
+ .allowUsersToSignUp(withValueOrThrow(true))
+ .projectVisibility(withValueOrThrow(true))
+ .userConsentRequiredAfterUpgrade(withValueOrThrow(true))
+ .build();
+
+ GithubConfiguration githubConfiguration = githubConfigurationService.updateConfiguration(updateRequest);
+
+ verifySettingWasSet(GITHUB_ENABLED, "true");
+ verifySettingWasSet(GITHUB_CLIENT_ID, "clientId");
+ verifySettingWasSet(GITHUB_CLIENT_SECRET, "clientSecret");
+ verifySettingWasSet(GITHUB_APP_ID, "applicationId");
+ verifySettingWasSet(GITHUB_PRIVATE_KEY, "privateKey");
+ verifySettingWasSet(GITHUB_GROUPS_SYNC, "true");
+ verifySettingWasSet(GITHUB_API_URL, "apiUrl");
+ verifySettingWasSet(GITHUB_WEB_URL, "webUrl");
+ verifySettingWasSet(GITHUB_ORGANIZATIONS, "org1,org2,org3");
+ verifyInternalSettingWasSet(GITHUB_PROVISIONING, "true");
+ verifySettingWasSet(GITHUB_ALLOW_USERS_TO_SIGN_UP, "true");
+ verifySettingWasSet(GITHUB_PROVISION_PROJECT_VISIBILITY, "true");
+ verifySettingExistsButEmpty(GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE);
+ verify(managedInstanceService).queueSynchronisationTask();
+
+ assertConfigurationFields(githubConfiguration);
+ }
+
+ @Test
+ public void updateConfiguration_whenAllUpdateFieldDefinedAndSetToFalse_updatesEverything() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(AUTO_PROVISIONING));
+ verify(managedInstanceService).queueSynchronisationTask();
+ clearInvocations(managedInstanceService);
+
+ UpdateGithubConfigurationRequest updateRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .enabled(withValueOrThrow(false))
+ .synchronizeGroups(withValueOrThrow(false))
+ .provisioningType(withValueOrThrow(JIT))
+ .allowUsersToSignUp(withValueOrThrow(false))
+ .build();
+
+ githubConfigurationService.updateConfiguration(updateRequest);
+
+ verifySettingWasSet(GITHUB_ENABLED, "false");
+ verifySettingWasSet(GITHUB_GROUPS_SYNC, "false");
+ verifyInternalSettingWasSet(GITHUB_PROVISIONING, "false");
+ verifySettingWasSet(GITHUB_ALLOW_USERS_TO_SIGN_UP, "false");
+ verifyNoMoreInteractions(managedInstanceService);
+
+ }
+
+ @Test
+ public void updateConfiguration_whenSwitchingFromAutoToJit_shouldNotScheduleSyncAndCallManagedInstanceChecker() {
+ DbSession dbSession = dbTester.getSession();
+ dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("12", "12", GitHubIdentityProvider.KEY));
+ dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("34", "34", GitHubIdentityProvider.KEY));
+ dbTester.getDbClient().githubOrganizationGroupDao().insert(dbSession, new GithubOrganizationGroupDto("14", "org1", "group1"));
+ dbSession.commit();
+
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(AUTO_PROVISIONING));
+ verify(managedInstanceService).queueSynchronisationTask();
+ reset(managedInstanceService);
+
+ UpdateGithubConfigurationRequest updateRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .provisioningType(withValueOrThrow(JIT))
+ .build();
+
+ githubConfigurationService.updateConfiguration(updateRequest);
+
+ verifyNoMoreInteractions(managedInstanceService);
+ assertThat(dbTester.getDbClient().externalGroupDao().selectByIdentityProvider(dbTester.getSession(), GitHubIdentityProvider.KEY)).isEmpty();
+ assertThat(dbTester.getDbClient().githubOrganizationGroupDao().findAll(dbTester.getSession())).isEmpty();
+ }
+
+ @Test
+ public void updateConfiguration_whenSwitchingToAutoProvisioningAndTheConfigIsNotEnabled_shouldThrow() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(JIT));
+
+ UpdateGithubConfigurationRequest disableRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .enabled(withValueOrThrow(false))
+ .build();
+
+ githubConfigurationService.updateConfiguration(disableRequest);
+
+ UpdateGithubConfigurationRequest updateRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .provisioningType(withValueOrThrow(AUTO_PROVISIONING))
+ .build();
+
+ assertThatThrownBy(() -> githubConfigurationService.updateConfiguration(updateRequest))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("GitHub authentication must be turned on to enable GitHub provisioning.");
+ verify(managedInstanceService, times(0)).queueSynchronisationTask();
+ }
+
+ @Test
+ public void updateConfiguration_whenURLChangesWithoutSecret_shouldFail() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(JIT));
+
+ UpdateGithubConfigurationRequest updateUrlRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .apiUrl(withValueOrThrow("http://malicious.url"))
+ .build();
+
+ assertThatThrownBy(() -> githubConfigurationService.updateConfiguration(updateUrlRequest))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("For security reasons, API and Web urls can't be updated without providing the private key.");
+ }
+
+ @Test
+ public void updateConfiguration_whenURLChangesWithAllSecrets_shouldUpdate() {
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(JIT));
+
+ UpdateGithubConfigurationRequest updateUrlRequest = builder()
+ .githubConfigurationId(UNIQUE_GITHUB_CONFIGURATION_ID)
+ .apiUrl(withValueOrThrow("http://new.url"))
+ .privateKey(withValueOrThrow("new-private-key"))
+ .build();
+
+ githubConfigurationService.updateConfiguration(updateUrlRequest);
+
+ verifySettingWasSet(GITHUB_API_URL, "http://new.url");
+ verifySettingWasSet(GITHUB_PRIVATE_KEY, "new-private-key");
+ }
+
+ private static void assertConfigurationFields(GithubConfiguration configuration) {
+ assertThat(configuration).isNotNull();
+ assertThat(configuration.id()).isEqualTo("github-configuration");
+ assertThat(configuration.enabled()).isTrue();
+ assertThat(configuration.clientId()).isEqualTo("clientId");
+ assertThat(configuration.clientSecret()).isEqualTo("clientSecret");
+ assertThat(configuration.applicationId()).isEqualTo("applicationId");
+ assertThat(configuration.privateKey()).isEqualTo("privateKey");
+ assertThat(configuration.synchronizeGroups()).isTrue();
+ assertThat(configuration.apiUrl()).isEqualTo("apiUrl");
+ assertThat(configuration.webUrl()).isEqualTo("webUrl");
+ assertThat(configuration.allowedOrganizations()).containsExactlyInAnyOrder("org1", "org2", "org3");
+ assertThat(configuration.provisioningType()).isEqualTo(AUTO_PROVISIONING);
+ assertThat(configuration.allowUsersToSignUp()).isTrue();
+ assertThat(configuration.provisionProjectVisibility()).isTrue();
+ assertThat(configuration.userConsentRequiredAfterUpgrade()).isTrue();
+ }
+
+ @Test
+ public void createConfiguration_whenConfigurationAlreadyExists_shouldThrow() {
+ GithubConfiguration githubConfiguration = buildGithubConfiguration(AUTO_PROVISIONING);
+ githubConfigurationService.createConfiguration(githubConfiguration);
+
+ assertThatThrownBy(() -> githubConfigurationService.createConfiguration(githubConfiguration))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("GitHub configuration already exists. Only one GitHub configuration is supported.");
+ }
+
+ @Test
+ public void createConfiguration_whenAutoProvisioning_shouldCreateCorrectConfigurationAndScheduleSync() {
+ GithubConfiguration configuration = buildGithubConfiguration(AUTO_PROVISIONING);
+
+ GithubConfiguration createdConfiguration = githubConfigurationService.createConfiguration(configuration);
+
+ assertConfigurationIsCorrect(configuration, createdConfiguration);
+
+ verifyCommonSettings(configuration);
+
+ verify(managedInstanceService).queueSynchronisationTask();
+
+ }
+
+ @Test
+ public void createConfiguration_whenInstanceIsExternallyManaged_shouldThrow() {
+ GithubConfiguration configuration = buildGithubConfiguration(AUTO_PROVISIONING);
+
+ when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
+ when(managedInstanceService.getProviderName()).thenReturn("not-github");
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> githubConfigurationService.createConfiguration(configuration))
+ .withMessage("It is not possible to synchronize SonarQube using GitHub, as it is already managed by not-github.");
+
+ }
+
+ @Test
+ public void createConfiguration_whenJitProvisioning_shouldCreateCorrectConfiguration() {
+ GithubConfiguration configuration = buildGithubConfiguration(JIT);
+
+ GithubConfiguration createdConfiguration = githubConfigurationService.createConfiguration(configuration);
+
+ assertConfigurationIsCorrect(configuration, createdConfiguration);
+
+ verifyCommonSettings(configuration);
+ verifyNoInteractions(managedInstanceService);
+
+ }
+
+ private void verifyCommonSettings(GithubConfiguration configuration) {
+ verifySettingWasSet(GITHUB_ENABLED, String.valueOf(configuration.enabled()));
+ verifySettingWasSet(GITHUB_CLIENT_ID, configuration.clientId());
+ verifySettingWasSet(GITHUB_CLIENT_SECRET, configuration.clientSecret());
+ verifySettingWasSet(GITHUB_APP_ID, configuration.applicationId());
+ verifySettingWasSet(GITHUB_PRIVATE_KEY, configuration.privateKey());
+ verifySettingWasSet(GITHUB_GROUPS_SYNC, String.valueOf(configuration.synchronizeGroups()));
+ verifySettingWasSet(GITHUB_API_URL, configuration.apiUrl());
+ verifySettingWasSet(GITHUB_WEB_URL, configuration.webUrl());
+ verifySettingWasSet(GITHUB_ORGANIZATIONS, String.join(",", configuration.allowedOrganizations()));
+ verifyInternalSettingWasSet(GITHUB_PROVISIONING, String.valueOf(configuration.provisioningType().equals(AUTO_PROVISIONING)));
+ verifySettingWasSet(GITHUB_ALLOW_USERS_TO_SIGN_UP, String.valueOf(configuration.allowUsersToSignUp()));
+ verifySettingWasSet(GITHUB_PROVISION_PROJECT_VISIBILITY, String.valueOf(configuration.provisionProjectVisibility()));
+ verifySettingExistsButEmpty(GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE);
+ }
+
+ private void verifySettingWasSet(String setting, @Nullable String value) {
+ assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty(setting).getValue()).isEqualTo(value);
+ }
+
+ private void verifyInternalSettingWasSet(String setting, @Nullable String value) {
+ assertThat(dbTester.getDbClient().internalPropertiesDao().selectByKey(dbTester.getSession(), setting)).contains(value);
+ }
+
+ private void verifySettingExistsButEmpty(String setting) {
+ assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty(setting)).isNotNull();
+ }
+
+ @Test
+ public void deleteConfiguration_whenIdIsNotGithubConfiguration_throwsException() {
+ assertThatThrownBy(() -> githubConfigurationService.deleteConfiguration("not-github-configuration"))
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("GitHub configuration with id not-github-configuration not found");
+ }
+
+ @Test
+ public void deleteConfiguration_whenConfigurationDoesntExist_throwsException() {
+ assertThatThrownBy(() -> githubConfigurationService.deleteConfiguration("github-configuration"))
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("GitHub configuration doesn't exist.");
+ }
+
+ @Test
+ public void deleteConfiguration_whenConfigurationExists_shouldDeleteConfiguration() {
+ DbSession dbSession = dbTester.getSession();
+ dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("12", "12", GitHubIdentityProvider.KEY));
+ dbTester.getDbClient().externalGroupDao().insert(dbSession, new ExternalGroupDto("34", "34", GitHubIdentityProvider.KEY));
+ dbSession.commit();
+ githubConfigurationService.createConfiguration(buildGithubConfiguration(AUTO_PROVISIONING));
+ githubConfigurationService.deleteConfiguration("github-configuration");
+
+ assertPropertyIsDeleted(GITHUB_ENABLED);
+ assertPropertyIsDeleted(GITHUB_CLIENT_ID);
+ assertPropertyIsDeleted(GITHUB_CLIENT_SECRET);
+ assertPropertyIsDeleted(GITHUB_APP_ID);
+ assertPropertyIsDeleted(GITHUB_PRIVATE_KEY);
+ assertPropertyIsDeleted(GITHUB_GROUPS_SYNC);
+ assertPropertyIsDeleted(GITHUB_API_URL);
+ assertPropertyIsDeleted(GITHUB_WEB_URL);
+ assertPropertyIsDeleted(GITHUB_ORGANIZATIONS);
+ assertInternalPropertyIsDeleted(GITHUB_PROVISIONING);
+ assertPropertyIsDeleted(GITHUB_ALLOW_USERS_TO_SIGN_UP);
+ assertPropertyIsDeleted(GITHUB_PROVISION_PROJECT_VISIBILITY);
+ assertPropertyIsDeleted(GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE);
+
+ assertThat(dbTester.getDbClient().externalGroupDao().selectByIdentityProvider(dbTester.getSession(), GitHubIdentityProvider.KEY)).isEmpty();
+ }
+
+ private void assertPropertyIsDeleted(String property) {
+ assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty(property)).isNull();
+ }
+
+ private void assertInternalPropertyIsDeleted(String property) {
+ assertThat(dbTester.getDbClient().internalPropertiesDao().selectByKey(dbTester.getSession(), property)).isEmpty();
+ }
+
+ @Test
+ public void validate_whenConfigurationIsDisabled_shouldNotValidate() {
+ GithubConfiguration githubConfiguration = buildGithubConfiguration(AUTO_PROVISIONING);
+ when(githubConfiguration.enabled()).thenReturn(false);
+
+ githubConfigurationService.validate(githubConfiguration);
+
+ verifyNoInteractions(githubGlobalSettingsValidator);
+ }
+
+ @Test
+ public void validate_whenConfigurationIsValidAndJIT_returnEmptyOptional() {
+ GithubConfiguration configuration = buildGithubConfiguration(JIT);
+ when(configuration.enabled()).thenReturn(true);
+
+ githubConfigurationService.validate(configuration);
+
+ verify(githubGlobalSettingsValidator)
+ .validate(configuration.applicationId(), configuration.clientId(), configuration.clientSecret(), configuration.privateKey(), configuration.apiUrl());
+ }
+
+ @Test
+ public void validate_whenConfigurationIsValidAndAutoProvisioning_returnEmptyOptional() {
+ GithubConfiguration configuration = buildGithubConfiguration(AUTO_PROVISIONING);
+ when(configuration.enabled()).thenReturn(true);
+
+ githubConfigurationService.validate(configuration);
+
+ verify(githubGlobalSettingsValidator)
+ .validate(configuration.applicationId(), configuration.clientId(), configuration.clientSecret(), configuration.privateKey(), configuration.apiUrl());
+ }
+
+ @Test
+ public void validate_whenConfigurationIsInValid_returnsExceptionMessage() {
+ GithubConfiguration configuration = buildGithubConfiguration(AUTO_PROVISIONING);
+ when(configuration.enabled()).thenReturn(true);
+
+ Exception exception = new IllegalStateException("Invalid configuration");
+ when(githubConfigurationService.validate(configuration)).thenThrow(exception);
+
+ Optional<String> message = githubConfigurationService.validate(configuration);
+
+ assertThat(message).contains("Invalid configuration");
+ }
+
+ private static GithubConfiguration buildGithubConfiguration(ProvisioningType provisioningType) {
+ GithubConfiguration githubConfiguration = mock();
+ when(githubConfiguration.id()).thenReturn("github-configuration");
+ when(githubConfiguration.enabled()).thenReturn(true);
+ when(githubConfiguration.clientId()).thenReturn("clientId");
+ when(githubConfiguration.clientSecret()).thenReturn("clientSecret");
+ when(githubConfiguration.applicationId()).thenReturn("applicationId");
+ when(githubConfiguration.privateKey()).thenReturn("privateKey");
+ when(githubConfiguration.synchronizeGroups()).thenReturn(true);
+ when(githubConfiguration.apiUrl()).thenReturn("apiUrl");
+ when(githubConfiguration.webUrl()).thenReturn("webUrl");
+ when(githubConfiguration.allowedOrganizations()).thenReturn(new LinkedHashSet<>(Set.of("org1", "org2", "org3")));
+ when(githubConfiguration.provisioningType()).thenReturn(provisioningType);
+ when(githubConfiguration.allowUsersToSignUp()).thenReturn(true);
+ when(githubConfiguration.provisionProjectVisibility()).thenReturn(true);
+ when(githubConfiguration.userConsentRequiredAfterUpgrade()).thenReturn(true);
+ return githubConfiguration;
+ }
+
+ private static void assertConfigurationIsCorrect(GithubConfiguration expectedConfiguration, GithubConfiguration actualConfiguration) {
+ assertThat(actualConfiguration.id()).isEqualTo(expectedConfiguration.id());
+ assertThat(actualConfiguration.enabled()).isEqualTo(expectedConfiguration.enabled());
+ assertThat(actualConfiguration.clientId()).isEqualTo(expectedConfiguration.clientId());
+ assertThat(actualConfiguration.clientSecret()).isEqualTo(expectedConfiguration.clientSecret());
+ assertThat(actualConfiguration.applicationId()).isEqualTo(expectedConfiguration.applicationId());
+ assertThat(actualConfiguration.privateKey()).isEqualTo(expectedConfiguration.privateKey());
+ assertThat(actualConfiguration.synchronizeGroups()).isEqualTo(expectedConfiguration.synchronizeGroups());
+ assertThat(actualConfiguration.apiUrl()).isEqualTo(expectedConfiguration.apiUrl());
+ assertThat(actualConfiguration.webUrl()).isEqualTo(expectedConfiguration.webUrl());
+ assertThat(actualConfiguration.allowedOrganizations()).containsExactlyInAnyOrderElementsOf(expectedConfiguration.allowedOrganizations());
+ assertThat(actualConfiguration.provisioningType()).isEqualTo(expectedConfiguration.provisioningType());
+ assertThat(actualConfiguration.allowUsersToSignUp()).isEqualTo(expectedConfiguration.allowUsersToSignUp());
+ assertThat(actualConfiguration.provisionProjectVisibility()).isEqualTo(expectedConfiguration.provisionProjectVisibility());
+ assertThat(actualConfiguration.userConsentRequiredAfterUpgrade()).isEqualTo(expectedConfiguration.userConsentRequiredAfterUpgrade());
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfiguration.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfiguration.java
new file mode 100644
index 00000000000..2ef170252ef
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.github.config;
+
+import java.util.Set;
+import org.sonar.server.common.gitlab.config.ProvisioningType;
+
+public record GithubConfiguration(
+ String id,
+ boolean enabled,
+ String clientId,
+ String clientSecret,
+ String applicationId,
+ String privateKey,
+ boolean synchronizeGroups,
+ String apiUrl,
+ String webUrl,
+ Set<String> allowedOrganizations,
+ ProvisioningType provisioningType,
+ boolean allowUsersToSignUp,
+ boolean provisionProjectVisibility,
+ boolean userConsentRequiredAfterUpgrade
+) {
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfigurationService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfigurationService.java
new file mode 100644
index 00000000000..16c1f62d85a
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/GithubConfigurationService.java
@@ -0,0 +1,352 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.github.config;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.api.server.ServerSide;
+import org.sonar.auth.github.GitHubIdentityProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.common.UpdatedValue;
+import org.sonar.server.common.gitlab.config.ProvisioningType;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.setting.ThreadLocalSettings;
+
+import static java.lang.String.format;
+import static org.sonar.api.utils.Preconditions.checkState;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ALLOW_USERS_TO_SIGN_UP;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_API_URL;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_APP_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_ID;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_CLIENT_SECRET;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ENABLED;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_GROUPS_SYNC;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_ORGANIZATIONS;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PRIVATE_KEY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PROVISIONING;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_PROVISION_PROJECT_VISIBILITY;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_WEB_URL;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
+import static org.sonar.server.exceptions.NotFoundException.checkFound;
+import static org.sonarqube.ws.WsUtils.checkArgument;
+
+@ServerSide
+public class GithubConfigurationService {
+
+ private static final List<String> GITHUB_CONFIGURATION_PROPERTIES = List.of(
+ GITHUB_ENABLED,
+ GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET,
+ GITHUB_APP_ID,
+ GITHUB_PRIVATE_KEY,
+ GITHUB_GROUPS_SYNC,
+ GITHUB_API_URL,
+ GITHUB_WEB_URL,
+ GITHUB_ORGANIZATIONS,
+ GITHUB_ALLOW_USERS_TO_SIGN_UP,
+ GITHUB_PROVISION_PROJECT_VISIBILITY,
+ GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE);
+
+ public static final String UNIQUE_GITHUB_CONFIGURATION_ID = "github-configuration";
+ private final DbClient dbClient;
+ private final ManagedInstanceService managedInstanceService;
+ private final GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+ private final ThreadLocalSettings threadLocalSettings;
+
+ public GithubConfigurationService(DbClient dbClient,
+ ManagedInstanceService managedInstanceService, GithubGlobalSettingsValidator githubGlobalSettingsValidator, ThreadLocalSettings threadLocalSettings) {
+ this.dbClient = dbClient;
+ this.managedInstanceService = managedInstanceService;
+ this.githubGlobalSettingsValidator = githubGlobalSettingsValidator;
+ this.threadLocalSettings = threadLocalSettings;
+ }
+
+ public GithubConfiguration updateConfiguration(UpdateGithubConfigurationRequest updateRequest) {
+ UpdatedValue<Boolean> provisioningEnabled = updateRequest.provisioningType().map(GithubConfigurationService::isTypeAutoProvisioning);
+ throwIfUrlIsUpdatedWithoutPrivateKey(updateRequest);
+ try (DbSession dbSession = dbClient.openSession(true)) {
+ throwIfConfigurationDoesntExist(dbSession);
+ GithubConfiguration currentConfiguration = getConfiguration(updateRequest.githubConfigurationId(), dbSession);
+
+ setIfDefined(dbSession, GITHUB_ENABLED, updateRequest.enabled().map(String::valueOf));
+ setIfDefined(dbSession, GITHUB_CLIENT_ID, updateRequest.clientId());
+ setIfDefined(dbSession, GITHUB_CLIENT_SECRET, updateRequest.clientSecret());
+ setIfDefined(dbSession, GITHUB_APP_ID, updateRequest.applicationId());
+ setIfDefined(dbSession, GITHUB_PRIVATE_KEY, updateRequest.privateKey());
+ setIfDefined(dbSession, GITHUB_GROUPS_SYNC, updateRequest.synchronizeGroups().map(String::valueOf));
+ setIfDefined(dbSession, GITHUB_API_URL, updateRequest.apiUrl());
+ setIfDefined(dbSession, GITHUB_WEB_URL, updateRequest.webUrl());
+ setIfDefined(dbSession, GITHUB_ORGANIZATIONS, updateRequest.allowedOrganizations().map(orgs -> String.join(",", orgs)));
+ setInternalIfDefined(dbSession, GITHUB_PROVISIONING, provisioningEnabled.map(String::valueOf));
+ setIfDefined(dbSession, GITHUB_ALLOW_USERS_TO_SIGN_UP, updateRequest.allowUsersToSignUp().map(String::valueOf));
+ setIfDefined(dbSession, GITHUB_PROVISION_PROJECT_VISIBILITY, updateRequest.projectVisibility().map(String::valueOf));
+ insertOrDeleteAsEmptyIfDefined(dbSession, GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE, updateRequest.userConsentRequiredAfterUpgrade().contains(true));
+
+ deleteExternalGroupsWhenDisablingAutoProvisioning(dbSession, currentConfiguration, updateRequest.provisioningType());
+ dbSession.commit();
+
+ GithubConfiguration updatedConfiguration = getConfiguration(UNIQUE_GITHUB_CONFIGURATION_ID, dbSession);
+ if (shouldTriggerProvisioning(provisioningEnabled, currentConfiguration)) {
+ triggerRun(updatedConfiguration);
+ }
+
+ return updatedConfiguration;
+ }
+ }
+
+ private static boolean shouldTriggerProvisioning(UpdatedValue<Boolean> provisioningEnabled, GithubConfiguration currentConfiguration) {
+ return provisioningEnabled.orElse(false) && !currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
+ }
+
+ private static void throwIfUrlIsUpdatedWithoutPrivateKey(UpdateGithubConfigurationRequest request) {
+ if (request.apiUrl().isDefined() || request.webUrl().isDefined()) {
+ checkArgument(request.privateKey().isDefined(), "For security reasons, API and Web urls can't be updated without providing the private key.");
+ }
+ }
+
+ private void setIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
+ value
+ .map(definedValue -> new PropertyDto().setKey(propertyName).setValue(definedValue))
+ .applyIfDefined(property -> dbClient.propertiesDao().saveProperty(dbSession, property));
+ threadLocalSettings.setProperty(propertyName, value.orElse(null));
+ }
+
+ private void insertOrDeleteAsEmptyIfDefined(DbSession dbSession, String propertyName, boolean value) {
+ if (value) {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName));
+ } else {
+ dbClient.propertiesDao().deleteGlobalProperty(propertyName, dbSession);
+ }
+ threadLocalSettings.setProperty(propertyName, value);
+ }
+
+ private void setInternalIfDefined(DbSession dbSession, String propertyName, UpdatedValue<String> value) {
+ value.applyIfDefined(v -> dbClient.internalPropertiesDao().save(dbSession, propertyName, v));
+ }
+
+ private void deleteExternalGroupsWhenDisablingAutoProvisioning(DbSession dbSession, GithubConfiguration currentConfiguration,
+ UpdatedValue<ProvisioningType> provisioningTypeFromUpdate) {
+ if (shouldDisableAutoProvisioning(currentConfiguration, provisioningTypeFromUpdate)) {
+ dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitHubIdentityProvider.KEY);
+ dbClient.githubOrganizationGroupDao().deleteAll(dbSession);
+ dbSession.commit();
+ }
+ }
+
+ private static boolean shouldDisableAutoProvisioning(GithubConfiguration currentConfiguration, UpdatedValue<ProvisioningType> provisioningTypeFromUpdate) {
+ return provisioningTypeFromUpdate.map(provisioningType -> provisioningType.equals(JIT)).orElse(false)
+ && currentConfiguration.provisioningType().equals(AUTO_PROVISIONING);
+ }
+
+ public GithubConfiguration getConfiguration(String id) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ throwIfNotUniqueConfigurationId(id);
+ throwIfConfigurationDoesntExist(dbSession);
+ return getConfiguration(id, dbSession);
+ }
+ }
+
+ public Optional<GithubConfiguration> findConfigurations() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ if (dbClient.propertiesDao().selectGlobalProperty(dbSession, GITHUB_ENABLED) == null) {
+ return Optional.empty();
+ }
+ return Optional.of(getConfiguration(UNIQUE_GITHUB_CONFIGURATION_ID, dbSession));
+ }
+ }
+
+ private Boolean getBooleanOrFalse(DbSession dbSession, String property) {
+ return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+ .map(dto -> Boolean.valueOf(dto.getValue())).orElse(false);
+ }
+
+ private Boolean getBooleanOrFalseFromEmptyProperty(DbSession dbSession, String property) {
+ return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+ .isPresent();
+ }
+
+ private Boolean getInternalBooleanOrFalse(DbSession dbSession, String property) {
+ return dbClient.internalPropertiesDao().selectByKey(dbSession, property)
+ .map(Boolean::valueOf)
+ .orElse(false);
+ }
+
+ private String getStringPropertyOrEmpty(DbSession dbSession, String property) {
+ return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, property))
+ .map(PropertyDto::getValue).orElse("");
+ }
+
+ private static void throwIfNotUniqueConfigurationId(String id) {
+ if (!UNIQUE_GITHUB_CONFIGURATION_ID.equals(id)) {
+ throw new NotFoundException(format("GitHub configuration with id %s not found", id));
+ }
+ }
+
+ public void deleteConfiguration(String id) {
+ throwIfNotUniqueConfigurationId(id);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ throwIfConfigurationDoesntExist(dbSession);
+ GITHUB_CONFIGURATION_PROPERTIES.forEach(property -> dbClient.propertiesDao().deleteGlobalProperty(property, dbSession));
+ dbClient.internalPropertiesDao().delete(dbSession, GITHUB_PROVISIONING);
+ dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitHubIdentityProvider.KEY);
+ dbSession.commit();
+ }
+ }
+
+ private void throwIfConfigurationDoesntExist(DbSession dbSession) {
+ checkFound(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITHUB_ENABLED), "GitHub configuration doesn't exist.");
+ }
+
+ private static ProvisioningType toProvisioningType(boolean provisioningEnabled) {
+ return provisioningEnabled ? AUTO_PROVISIONING : JIT;
+ }
+
+ public GithubConfiguration createConfiguration(GithubConfiguration configuration) {
+ throwIfConfigurationAlreadyExists();
+
+ boolean enableAutoProvisioning = isTypeAutoProvisioning(configuration.provisioningType());
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ setProperty(dbSession, GITHUB_ENABLED, String.valueOf(configuration.enabled()));
+ setProperty(dbSession, GITHUB_CLIENT_ID, configuration.clientId());
+ setProperty(dbSession, GITHUB_CLIENT_SECRET, configuration.clientSecret());
+ setProperty(dbSession, GITHUB_APP_ID, configuration.applicationId());
+ setProperty(dbSession, GITHUB_PRIVATE_KEY, configuration.privateKey());
+ setProperty(dbSession, GITHUB_GROUPS_SYNC, String.valueOf(configuration.synchronizeGroups()));
+ setProperty(dbSession, GITHUB_API_URL, configuration.apiUrl());
+ setProperty(dbSession, GITHUB_WEB_URL, configuration.webUrl());
+ setProperty(dbSession, GITHUB_ORGANIZATIONS, String.join(",", configuration.allowedOrganizations()));
+ setInternalProperty(dbSession, GITHUB_PROVISIONING, String.valueOf(enableAutoProvisioning));
+ setProperty(dbSession, GITHUB_ALLOW_USERS_TO_SIGN_UP, String.valueOf(configuration.allowUsersToSignUp()));
+ setProperty(dbSession, GITHUB_PROVISION_PROJECT_VISIBILITY, String.valueOf(configuration.provisionProjectVisibility()));
+ setPropertyAsEmpty(dbSession, GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE, configuration.userConsentRequiredAfterUpgrade());
+ if (enableAutoProvisioning) {
+ triggerRun(configuration);
+ }
+ GithubConfiguration createdConfiguration = getConfiguration(UNIQUE_GITHUB_CONFIGURATION_ID, dbSession);
+ dbSession.commit();
+ return createdConfiguration;
+ }
+
+ }
+
+ private void throwIfConfigurationAlreadyExists() {
+ Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(GITHUB_ENABLED)).ifPresent(property -> {
+ throw BadRequestException.create("GitHub configuration already exists. Only one GitHub configuration is supported.");
+ });
+ }
+
+ private static boolean isTypeAutoProvisioning(ProvisioningType provisioningType) {
+ return AUTO_PROVISIONING.equals(provisioningType);
+ }
+
+ private void setProperty(DbSession dbSession, String propertyName, @Nullable String value) {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName).setValue(value));
+ }
+
+ private void setPropertyAsEmpty(DbSession dbSession, String propertyName, boolean value) {
+ if (value) {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(propertyName));
+ }
+ }
+
+ private void setInternalProperty(DbSession dbSession, String propertyName, @Nullable String value) {
+ if (StringUtils.isNotEmpty(value)) {
+ dbClient.internalPropertiesDao().save(dbSession, propertyName, value);
+ }
+ }
+
+ private GithubConfiguration getConfiguration(String id, DbSession dbSession) {
+ throwIfNotUniqueConfigurationId(id);
+ throwIfConfigurationDoesntExist(dbSession);
+ return new GithubConfiguration(
+ UNIQUE_GITHUB_CONFIGURATION_ID,
+ getBooleanOrFalse(dbSession, GITHUB_ENABLED),
+ getStringPropertyOrEmpty(dbSession, GITHUB_CLIENT_ID),
+ getStringPropertyOrEmpty(dbSession, GITHUB_CLIENT_SECRET),
+ getStringPropertyOrEmpty(dbSession, GITHUB_APP_ID),
+ getStringPropertyOrEmpty(dbSession, GITHUB_PRIVATE_KEY),
+ getBooleanOrFalse(dbSession, GITHUB_GROUPS_SYNC),
+ getStringPropertyOrEmpty(dbSession, GITHUB_API_URL),
+ getStringPropertyOrEmpty(dbSession, GITHUB_WEB_URL),
+ getAllowedOrganizations(dbSession),
+ toProvisioningType(getInternalBooleanOrFalse(dbSession, GITHUB_PROVISIONING)),
+ getBooleanOrFalse(dbSession, GITHUB_ALLOW_USERS_TO_SIGN_UP),
+ getBooleanOrFalse(dbSession, GITHUB_PROVISION_PROJECT_VISIBILITY),
+ getBooleanOrFalseFromEmptyProperty(dbSession, GITHUB_USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE));
+ }
+
+ private Set<String> getAllowedOrganizations(DbSession dbSession) {
+ return Optional.ofNullable(dbClient.propertiesDao().selectGlobalProperty(dbSession, GITHUB_ORGANIZATIONS))
+ .map(dto -> Arrays.stream(dto.getValue().split(","))
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet()))
+ .orElse(Set.of());
+ }
+
+ private void triggerRun(GithubConfiguration githubConfiguration) {
+ throwIfConfigIncompleteOrInstanceAlreadyManaged(githubConfiguration);
+ managedInstanceService.queueSynchronisationTask();
+ }
+
+ private void throwIfConfigIncompleteOrInstanceAlreadyManaged(GithubConfiguration configuration) {
+ checkInstanceNotManagedByAnotherProvider();
+ checkState(AUTO_PROVISIONING.equals(configuration.provisioningType()), "Auto provisioning must be activated");
+ checkState(configuration.enabled(), getErrorMessage("GitHub authentication must be turned on"));
+ }
+
+ private void checkInstanceNotManagedByAnotherProvider() {
+ if (managedInstanceService.isInstanceExternallyManaged()) {
+ Optional.of(managedInstanceService.getProviderName()).filter(providerName -> !GitHubIdentityProvider.KEY.equals(providerName))
+ .ifPresent(providerName -> {
+ throw new IllegalStateException("It is not possible to synchronize SonarQube using GitHub, as it is already managed by " + providerName + ".");
+ });
+ }
+ }
+
+ private static String getErrorMessage(String prefix) {
+ return format("%s to enable GitHub provisioning.", prefix);
+ }
+
+ public Optional<String> validate(GithubConfiguration configuration) {
+ if (!configuration.enabled()) {
+ return Optional.empty();
+ }
+ try {
+ githubGlobalSettingsValidator.validate(configuration.applicationId(), configuration.clientId(), configuration.clientSecret(), configuration.privateKey(),
+ configuration.apiUrl());
+ } catch (Exception e) {
+ return Optional.of(e.getMessage());
+ }
+ return Optional.empty();
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/UpdateGithubConfigurationRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/UpdateGithubConfigurationRequest.java
new file mode 100644
index 00000000000..df50df0398a
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/UpdateGithubConfigurationRequest.java
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.common.github.config;
+
+import java.util.Set;
+import org.sonar.server.common.NonNullUpdatedValue;
+import org.sonar.server.common.gitlab.config.ProvisioningType;
+
+public record UpdateGithubConfigurationRequest(
+ String githubConfigurationId,
+ NonNullUpdatedValue<Boolean> enabled,
+ NonNullUpdatedValue<String> clientId,
+ NonNullUpdatedValue<String> clientSecret,
+ NonNullUpdatedValue<String> applicationId,
+ NonNullUpdatedValue<String> privateKey,
+ NonNullUpdatedValue<Boolean> synchronizeGroups,
+ NonNullUpdatedValue<String> apiUrl,
+ NonNullUpdatedValue<String> webUrl,
+ NonNullUpdatedValue<Set<String>> allowedOrganizations,
+ NonNullUpdatedValue<ProvisioningType> provisioningType,
+ NonNullUpdatedValue<Boolean> allowUsersToSignUp,
+ NonNullUpdatedValue<Boolean> projectVisibility,
+ NonNullUpdatedValue<Boolean> userConsentRequiredAfterUpgrade
+) {
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private String githubConfigurationId;
+ private NonNullUpdatedValue<Boolean> enabled = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> clientId = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> clientSecret = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> applicationId = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> privateKey = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<Boolean> synchronizeGroups = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> apiUrl = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<String> webUrl = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<Set<String>> allowedOrganizations = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<ProvisioningType> provisioningType = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<Boolean> allowUsersToSignUp = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<Boolean> projectVisibility = NonNullUpdatedValue.undefined();
+ private NonNullUpdatedValue<Boolean> userConsentRequiredAfterUpgrade = NonNullUpdatedValue.undefined();
+
+ private Builder() {
+ }
+
+ public Builder githubConfigurationId(String githubConfigurationId) {
+ this.githubConfigurationId = githubConfigurationId;
+ return this;
+ }
+
+ public Builder enabled(NonNullUpdatedValue<Boolean> enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ public Builder clientId(NonNullUpdatedValue<String> clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ public Builder clientSecret(NonNullUpdatedValue<String> clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public Builder applicationId(NonNullUpdatedValue<String> applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ public Builder privateKey(NonNullUpdatedValue<String> privateKey) {
+ this.privateKey = privateKey;
+ return this;
+ }
+
+ public Builder synchronizeGroups(NonNullUpdatedValue<Boolean> synchronizeGroups) {
+ this.synchronizeGroups = synchronizeGroups;
+ return this;
+ }
+
+ public Builder apiUrl(NonNullUpdatedValue<String> apiUrl) {
+ this.apiUrl = apiUrl;
+ return this;
+ }
+
+ public Builder webUrl(NonNullUpdatedValue<String> webUrl) {
+ this.webUrl = webUrl;
+ return this;
+ }
+
+ public Builder allowedOrganizations(NonNullUpdatedValue<Set<String>> allowedOrganizations) {
+ this.allowedOrganizations = allowedOrganizations;
+ return this;
+ }
+
+ public Builder provisioningType(NonNullUpdatedValue<ProvisioningType> provisioningType) {
+ this.provisioningType = provisioningType;
+ return this;
+ }
+
+ public Builder allowUsersToSignUp(NonNullUpdatedValue<Boolean> allowUsersToSignUp) {
+ this.allowUsersToSignUp = allowUsersToSignUp;
+ return this;
+ }
+
+ public Builder projectVisibility(NonNullUpdatedValue<Boolean> projectVisibility) {
+ this.projectVisibility = projectVisibility;
+ return this;
+ }
+
+ public Builder userConsentRequiredAfterUpgrade(NonNullUpdatedValue<Boolean> userConsentRequiredAfterUpgrade) {
+ this.userConsentRequiredAfterUpgrade = userConsentRequiredAfterUpgrade;
+ return this;
+ }
+
+ public UpdateGithubConfigurationRequest build() {
+ return new UpdateGithubConfigurationRequest(githubConfigurationId, enabled, clientId, clientSecret, applicationId, privateKey, synchronizeGroups, apiUrl, webUrl,
+ allowedOrganizations, provisioningType, allowUsersToSignUp, projectVisibility, userConsentRequiredAfterUpgrade);
+ }
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/package-info.java
new file mode 100644
index 00000000000..07850d8395e
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/config/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common.github.config;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
index a4e820e1be5..0bd6af1386b 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
@@ -39,6 +39,8 @@ public class WebApiEndpoints {
public static final String GITLAB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/gitlab-configurations";
+ public static final String GITHUB_CONFIGURATION_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/github-configurations";
+
public static final String BOUND_PROJECTS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/bound-projects";
public static final String PROJECT_BINDINGS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/project-bindings";
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/DefaultGithubConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/DefaultGithubConfigurationController.java
new file mode 100644
index 00000000000..03a69639c34
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/DefaultGithubConfigurationController.java
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.controller;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.server.common.github.config.GithubConfiguration;
+import org.sonar.server.common.github.config.GithubConfigurationService;
+import org.sonar.server.common.github.config.UpdateGithubConfigurationRequest;
+import org.sonar.server.common.gitlab.config.ProvisioningType;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.github.config.request.GithubConfigurationCreateRestRequest;
+import org.sonar.server.v2.api.github.config.request.GithubConfigurationUpdateRestRequest;
+import org.sonar.server.v2.api.github.config.resource.GithubConfigurationResource;
+import org.sonar.server.v2.api.github.config.response.GithubConfigurationSearchRestResponse;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.server.common.github.config.GithubConfigurationService.UNIQUE_GITHUB_CONFIGURATION_ID;
+
+public class DefaultGithubConfigurationController implements GithubConfigurationController {
+
+ private final UserSession userSession;
+ private final GithubConfigurationService githubConfigurationService;
+
+ public DefaultGithubConfigurationController(UserSession userSession, GithubConfigurationService githubConfigurationService) {
+ this.userSession = userSession;
+ this.githubConfigurationService = githubConfigurationService;
+ }
+
+ @Override
+ public GithubConfigurationResource getGithubConfiguration(String id) {
+ userSession.checkIsSystemAdministrator();
+ return getGithubConfigurationResource(id);
+ }
+
+ @Override
+ public GithubConfigurationSearchRestResponse searchGithubConfiguration() {
+ userSession.checkIsSystemAdministrator();
+
+ List<GithubConfigurationResource> githubConfigurationResources = githubConfigurationService.findConfigurations()
+ .stream()
+ .map(this::toGithubConfigurationResource)
+ .toList();
+
+ PageRestResponse pageRestResponse = new PageRestResponse(1, 1000, githubConfigurationResources.size());
+ return new GithubConfigurationSearchRestResponse(githubConfigurationResources, pageRestResponse);
+ }
+
+ @Override
+ public GithubConfigurationResource createGithubConfiguration(GithubConfigurationCreateRestRequest createRequest) {
+ userSession.checkIsSystemAdministrator();
+ GithubConfiguration createdConfiguration = githubConfigurationService.createConfiguration(toGithubConfiguration(createRequest));
+ return toGithubConfigurationResource(createdConfiguration);
+ }
+
+ private static GithubConfiguration toGithubConfiguration(GithubConfigurationCreateRestRequest createRestRequest) {
+ return new GithubConfiguration(
+ UNIQUE_GITHUB_CONFIGURATION_ID,
+ createRestRequest.enabled(),
+ createRestRequest.clientId(),
+ createRestRequest.clientSecret(),
+ createRestRequest.applicationId(),
+ createRestRequest.privateKey(),
+ createRestRequest.synchronizeGroups(),
+ createRestRequest.apiUrl(),
+ createRestRequest.webUrl(),
+ Set.copyOf(createRestRequest.allowedOrganizations()),
+ toProvisioningType(createRestRequest.provisioningType()),
+ createRestRequest.allowUsersToSignUp() != null && createRestRequest.allowUsersToSignUp(),
+ createRestRequest.projectVisibility() != null && createRestRequest.projectVisibility(),
+ createRestRequest.userConsentRequiredAfterUpgrade() != null && createRestRequest.userConsentRequiredAfterUpgrade());
+ }
+
+ private GithubConfigurationResource getGithubConfigurationResource(String id) {
+ return toGithubConfigurationResource(githubConfigurationService.getConfiguration(id));
+ }
+
+ @Override
+ public GithubConfigurationResource updateGithubConfiguration(String id, GithubConfigurationUpdateRestRequest updateRequest) {
+ userSession.checkIsSystemAdministrator();
+ UpdateGithubConfigurationRequest updateGithubConfigurationRequest = toUpdateGithubConfigurationRequest(id, updateRequest);
+ return toGithubConfigurationResource(githubConfigurationService.updateConfiguration(updateGithubConfigurationRequest));
+ }
+
+ private static UpdateGithubConfigurationRequest toUpdateGithubConfigurationRequest(String id, GithubConfigurationUpdateRestRequest updateRequest) {
+ return UpdateGithubConfigurationRequest.builder()
+ .githubConfigurationId(id)
+ .enabled(updateRequest.getEnabled().toNonNullUpdatedValue())
+ .clientId(updateRequest.getClientId().toNonNullUpdatedValue())
+ .clientSecret(updateRequest.getClientSecret().toNonNullUpdatedValue())
+ .applicationId(updateRequest.getApplicationId().toNonNullUpdatedValue())
+ .privateKey(updateRequest.getPrivateKey().toNonNullUpdatedValue())
+ .synchronizeGroups(updateRequest.getSynchronizeGroups().toNonNullUpdatedValue())
+ .apiUrl(updateRequest.getApiUrl().toNonNullUpdatedValue())
+ .webUrl(updateRequest.getWebUrl().toNonNullUpdatedValue())
+ .allowedOrganizations(updateRequest.getAllowedOrganizations().map(DefaultGithubConfigurationController::getOrganizations).toNonNullUpdatedValue())
+ .provisioningType(updateRequest.getProvisioningType().map(DefaultGithubConfigurationController::toProvisioningType).toNonNullUpdatedValue())
+ .allowUsersToSignUp(updateRequest.getAllowUsersToSignUp().toNonNullUpdatedValue())
+ .projectVisibility(updateRequest.getProjectVisibility().toNonNullUpdatedValue())
+ .userConsentRequiredAfterUpgrade(updateRequest.getUserConsentRequiredAfterUpgrade().toNonNullUpdatedValue())
+ .build();
+ }
+
+ private static Set<String> getOrganizations(@Nullable List<String> orgs) {
+ checkArgument(orgs != null, "allowedOrganizations must not be null");
+ return new HashSet<>(orgs);
+ }
+
+ private GithubConfigurationResource toGithubConfigurationResource(GithubConfiguration configuration) {
+ Optional<String> configurationError = githubConfigurationService.validate(configuration);
+ return new GithubConfigurationResource(
+ configuration.id(),
+ configuration.enabled(),
+ configuration.applicationId(),
+ configuration.synchronizeGroups(),
+ configuration.apiUrl(),
+ configuration.webUrl(),
+ sortGroups(configuration.allowedOrganizations()),
+ toRestProvisioningType(configuration),
+ configuration.allowUsersToSignUp(),
+ configuration.provisionProjectVisibility(),
+ configuration.userConsentRequiredAfterUpgrade(),
+ configurationError.orElse(null));
+ }
+
+ private static org.sonar.server.v2.api.model.ProvisioningType toRestProvisioningType(GithubConfiguration configuration) {
+ return org.sonar.server.v2.api.model.ProvisioningType.valueOf(configuration.provisioningType().name());
+ }
+
+ private static ProvisioningType toProvisioningType(org.sonar.server.v2.api.model.ProvisioningType provisioningType) {
+ return ProvisioningType.valueOf(provisioningType.name());
+ }
+
+ private static List<String> sortGroups(Set<String> groups) {
+ return groups.stream().sorted().toList();
+ }
+
+ @Override
+ public void deleteGithubConfiguration(String id) {
+ userSession.checkIsSystemAdministrator();
+ githubConfigurationService.deleteConfiguration(id);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java
new file mode 100644
index 00000000000..eb6865db0ee
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/GithubConfigurationController.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import javax.validation.Valid;
+import org.sonar.server.v2.api.github.config.request.GithubConfigurationCreateRestRequest;
+import org.sonar.server.v2.api.github.config.request.GithubConfigurationUpdateRestRequest;
+import org.sonar.server.v2.api.github.config.resource.GithubConfigurationResource;
+import org.sonar.server.v2.api.github.config.response.GithubConfigurationSearchRestResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.GITHUB_CONFIGURATION_ENDPOINT;
+import static org.sonar.server.v2.WebApiEndpoints.INTERNAL;
+import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+
+@RequestMapping(GITHUB_CONFIGURATION_ENDPOINT)
+@RestController
+public interface GithubConfigurationController {
+
+ @GetMapping(path = "/{id}")
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "Fetch a GitHub configuration", description = """
+ Fetch a GitHub configuration. Requires 'Administer System' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+ GithubConfigurationResource getGithubConfiguration(
+ @PathVariable("id") @Parameter(description = "The id of the configuration to fetch.", required = true, in = ParameterIn.PATH) String id);
+
+ @GetMapping
+ @Operation(summary = "Search GitHub configs", description = """
+ Get the list of GitHub configurations.
+ Note that a single configuration is supported at this time.
+ Requires 'Administer System' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+ GithubConfigurationSearchRestResponse searchGithubConfiguration();
+
+ @PatchMapping(path = "/{id}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "Update a GitHub configuration", description = """
+ Update a GitHub configuration. Requires 'Administer System' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+ GithubConfigurationResource updateGithubConfiguration(@PathVariable("id") String id, @Valid @RequestBody GithubConfigurationUpdateRestRequest updateRequest);
+
+ @PostMapping
+ @Operation(summary = "Create GitHub configuration", description = """
+ Create a new GitHub configuration.
+ Note that only a single configuration can exist at a time.
+ Requires 'Administer System' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+ GithubConfigurationResource createGithubConfiguration(@Valid @RequestBody GithubConfigurationCreateRestRequest createRequest);
+
+ @DeleteMapping(path = "/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(summary = "Delete a GitHub configuration", description = """
+ Delete a GitHub configuration.
+ Requires 'Administer System' permission.
+ """,
+ extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}))
+ void deleteGithubConfiguration(
+ @PathVariable("id") @Parameter(description = "The id of the configuration to delete.", required = true, in = ParameterIn.PATH) String id);
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/package-info.java
new file mode 100644
index 00000000000..af6e4b356a1
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/controller/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.github.config.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationCreateRestRequest.java
new file mode 100644
index 00000000000..d5cf09c7169
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationCreateRestRequest.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.request;
+
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import org.sonar.server.v2.api.model.ProvisioningType;
+
+public record GithubConfigurationCreateRestRequest(
+
+ @NotNull
+ @Schema(description = "Enable GitHub authentication")
+ boolean enabled,
+
+ @NotEmpty
+ @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = "Client ID provided by GitHub when registering the application.")
+ String clientId,
+
+ @NotEmpty
+ @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = "Client password provided by GitHub when registering the application.")
+ String clientSecret,
+
+ @NotEmpty
+ @Schema(description = "The App ID is found on your GitHub App's page on GitHub at Settings > Developer Settings > GitHub Apps.")
+ String applicationId,
+
+ @NotEmpty
+ @Schema(accessMode = Schema.AccessMode.WRITE_ONLY, description = """
+ Your GitHub App's private key. You can generate a .pem file from your GitHub App's page under Private keys.
+ Copy and paste the whole contents of the file here.
+ """)
+ String privateKey,
+
+ @NotNull
+ @Schema(description = """
+ Synchronize GitHub team with SonarQube group memberships when users log in to SonarQube.
+ For each GitHub team they belong to, users will be associated to a group of the same name if it exists in SonarQube.
+ """)
+ Boolean synchronizeGroups,
+
+ @NotEmpty
+ @Schema(description = "The API url for a GitHub instance. https://api.github.com/ for Github.com, https://github.company.com/api/v3/ when using Github Enterprise")
+ String apiUrl,
+
+ @NotEmpty
+ @Schema(description = "The WEB url for a GitHub instance. https://github.com/ for Github.com, https://github.company.com/ when using GitHub Enterprise.\n")
+ String webUrl,
+
+ @NotNull
+ @ArraySchema(arraySchema = @Schema(description = """
+ Only members of these organizations will be able to authenticate to the server.
+ ⚠ if not set, users from any organization where the GitHub App is installed will be able to login to this SonarQube instance.
+ """))
+ List<String> allowedOrganizations,
+
+ @NotNull
+ @Schema(description = "Type of synchronization")
+ ProvisioningType provisioningType,
+
+ @Nullable
+ @Schema(description = "Allow user to sign up")
+ Boolean allowUsersToSignUp,
+
+ @Nullable
+ @Schema(description = """
+ Change project visibility based on GitHub repository visibility.
+ If disabled, every provisioned project will be private in SonarQube and visible only to users with explicit GitHub permissions for the corresponding repository.
+ Changes take effect at the next synchronization.
+ """)
+ Boolean projectVisibility,
+
+ @Nullable
+ @Schema(description = "Admin consent to synchronize permissions from GitHub")
+ Boolean userConsentRequiredAfterUpgrade
+) {
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationUpdateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationUpdateRestRequest.java
new file mode 100644
index 00000000000..e25ebb1b22b
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/GithubConfigurationUpdateRestRequest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.request;
+
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import org.sonar.server.v2.api.model.ProvisioningType;
+import org.sonar.server.v2.common.model.UpdateField;
+
+public class GithubConfigurationUpdateRestRequest {
+
+ private UpdateField<Boolean> enabled = UpdateField.undefined();
+ private UpdateField<String> clientId = UpdateField.undefined();
+ private UpdateField<String> clientSecret = UpdateField.undefined();
+ private UpdateField<String> applicationId = UpdateField.undefined();
+ private UpdateField<String> privateKey = UpdateField.undefined();
+ private UpdateField<Boolean> synchronizeGroups = UpdateField.undefined();
+ private UpdateField<String> apiUrl = UpdateField.undefined();
+ private UpdateField<String> webUrl = UpdateField.undefined();
+ private UpdateField<List<String>> allowedOrganizations = UpdateField.undefined();
+ private UpdateField<ProvisioningType> provisioningType = UpdateField.undefined();
+ private UpdateField<Boolean> allowUsersToSignUp = UpdateField.undefined();
+ private UpdateField<Boolean> projectVisibility = UpdateField.undefined();
+ private UpdateField<Boolean> userConsentRequiredAfterUpgrade = UpdateField.undefined();
+
+ @Schema(implementation = Boolean.class, description = "Enable GitHub authentication")
+ public UpdateField<Boolean> getEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ this.enabled = UpdateField.withValue(enabled);
+ }
+
+ @Schema(implementation = String.class, description = "GitHub Client ID")
+ public UpdateField<String> getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = UpdateField.withValue(clientId);
+ }
+
+ @Schema(implementation = String.class, description = "GitHub Client secret")
+ public UpdateField<String> getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = UpdateField.withValue(clientSecret);
+ }
+
+ @Schema(implementation = String.class, description = "GitHub Application id")
+ public UpdateField<String> getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(String applicationId) {
+ this.applicationId = UpdateField.withValue(applicationId);
+ }
+
+ @Schema(implementation = String.class, description = "GitHub Private key")
+ public UpdateField<String> getPrivateKey() {
+ return privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = UpdateField.withValue(privateKey);
+ }
+
+ @Schema(implementation = Boolean.class, description = "Set whether to synchronize groups")
+ public UpdateField<Boolean> getSynchronizeGroups() {
+ return synchronizeGroups;
+ }
+
+ public void setSynchronizeGroups(Boolean synchronizeGroups) {
+ this.synchronizeGroups = UpdateField.withValue(synchronizeGroups);
+ }
+
+ @Schema(implementation = String.class, description = "Url of GitHub instance for API connectivity (for instance https://api.github.com)")
+ public UpdateField<String> getApiUrl() {
+ return apiUrl;
+ }
+
+ public void setApiUrl(String apiUrl) {
+ this.apiUrl = UpdateField.withValue(apiUrl);
+ }
+
+ @Schema(implementation = String.class, description = "Url of GitHub instance for authentication (for instance https://github.com)")
+ public UpdateField<String> getWebUrl() {
+ return webUrl;
+ }
+
+ public void setWebUrl(String webUrl) {
+ this.webUrl = UpdateField.withValue(webUrl);
+ }
+
+ @ArraySchema(arraySchema = @Schema(description = "GitHub organizations allowed to authenticate and provisioned"), schema = @Schema(implementation = String.class))
+ public UpdateField<List<String>> getAllowedOrganizations() {
+ return allowedOrganizations;
+ }
+
+ public void setAllowedOrganizations(List<String> allowedOrganizations) {
+ this.allowedOrganizations = UpdateField.withValue(allowedOrganizations);
+ }
+
+ @Schema(implementation = ProvisioningType.class, description = "Type of synchronization")
+ public UpdateField<ProvisioningType> getProvisioningType() {
+ return provisioningType;
+ }
+
+ public void setProvisioningType(ProvisioningType provisioningType) {
+ this.provisioningType = UpdateField.withValue(provisioningType);
+ }
+
+ @Schema(implementation = Boolean.class, description = "Allow user to sign up")
+ public UpdateField<Boolean> getAllowUsersToSignUp() {
+ return allowUsersToSignUp;
+ }
+
+ public void setAllowUsersToSignUp(Boolean allowUsersToSignUp) {
+ this.allowUsersToSignUp = UpdateField.withValue(allowUsersToSignUp);
+ }
+
+ @Schema(implementation = Boolean.class, description = "Sync project visibility")
+ public UpdateField<Boolean> getProjectVisibility() {
+ return projectVisibility;
+ }
+
+ public void setProjectVisibility(Boolean projectVisibility) {
+ this.projectVisibility = UpdateField.withValue(projectVisibility);
+ }
+
+ @Schema(implementation = Boolean.class, description = "Admin consent to synchronize permissions from GitHub")
+ public UpdateField<Boolean> getUserConsentRequiredAfterUpgrade() {
+ return userConsentRequiredAfterUpgrade;
+ }
+
+ public void setUserConsentRequiredAfterUpgrade(Boolean userConsentRequiredAfterUpgrade) {
+ this.userConsentRequiredAfterUpgrade = UpdateField.withValue(userConsentRequiredAfterUpgrade);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/package-info.java
new file mode 100644
index 00000000000..9f9c01a6fe3
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/request/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.github.config.request;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/GithubConfigurationResource.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/GithubConfigurationResource.java
new file mode 100644
index 00000000000..fba9b7fa9a5
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/GithubConfigurationResource.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.resource;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.server.v2.api.model.ProvisioningType;
+
+public record GithubConfigurationResource(
+
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+ String id,
+
+ boolean enabled,
+
+ @Schema(implementation = String.class, description = "GitHub Application id")
+ String applicationId,
+
+ boolean synchronizeGroups,
+
+ @Schema(description = "Url of GitHub instance for API connectivity (for instance https://api.github.com)")
+ String apiUrl,
+
+ @Schema(description = "Url of GitHub instance for authentication (for instance https://github.com)")
+ String webUrl,
+
+ @Schema(description = "GitHub organizations allowed to authenticate and provisioned")
+ List<String> allowedOrganizations,
+
+ ProvisioningType provisioningType,
+
+ boolean allowUsersToSignUp,
+
+ boolean projectVisibility,
+
+ boolean userConsentRequiredAfterUpgrade,
+
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY, description = "In case the GitHub configuration is incorrect, error message")
+ @Nullable
+ String errorMessage
+) {
+}
+
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/package-info.java
new file mode 100644
index 00000000000..059cdfa6a12
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/resource/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.github.config.resource;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/GithubConfigurationSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/GithubConfigurationSearchRestResponse.java
new file mode 100644
index 00000000000..c9c5c189928
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/GithubConfigurationSearchRestResponse.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config.response;
+
+import java.util.List;
+import org.sonar.server.v2.api.github.config.resource.GithubConfigurationResource;
+import org.sonar.server.v2.api.response.PageRestResponse;
+
+public record GithubConfigurationSearchRestResponse(List<GithubConfigurationResource> githubConfigurations, PageRestResponse page) {}
+
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/package-info.java
new file mode 100644
index 00000000000..5fa91f90eb2
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/config/response/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.github.config.response;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java
index d88fc9d8642..60bef449af6 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/controller/DefaultGitlabConfigurationController.java
@@ -136,11 +136,11 @@ public class DefaultGitlabConfigurationController implements GitlabConfiguration
configurationError.orElse(null));
}
- private static org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType toRestProvisioningType(GitlabConfiguration configuration) {
- return org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType.valueOf(configuration.provisioningType().name());
+ private static org.sonar.server.v2.api.model.ProvisioningType toRestProvisioningType(GitlabConfiguration configuration) {
+ return org.sonar.server.v2.api.model.ProvisioningType.valueOf(configuration.provisioningType().name());
}
- private static ProvisioningType toProvisioningType(org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType provisioningType) {
+ private static ProvisioningType toProvisioningType(org.sonar.server.v2.api.model.ProvisioningType provisioningType) {
return ProvisioningType.valueOf(provisioningType.name());
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java
index 5ba14e7db54..837328970ed 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationCreateRestRequest.java
@@ -25,7 +25,7 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
-import org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType;
+import org.sonar.server.v2.api.model.ProvisioningType;
public record GitlabConfigurationCreateRestRequest(
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java
index 6bc9f941b0d..57e38b911dd 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/request/GitlabConfigurationUpdateRestRequest.java
@@ -23,7 +23,7 @@ import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import javax.validation.constraints.Size;
-import org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType;
+import org.sonar.server.v2.api.model.ProvisioningType;
import org.sonar.server.v2.common.model.UpdateField;
public class GitlabConfigurationUpdateRestRequest {
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java
index d8de23c8e05..075dddf02e8 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/GitlabConfigurationResource.java
@@ -22,6 +22,7 @@ package org.sonar.server.v2.api.gitlab.config.resource;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import javax.annotation.Nullable;
+import org.sonar.server.v2.api.model.ProvisioningType;
public record GitlabConfigurationResource(
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/ProvisioningType.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/ProvisioningType.java
index 3641019264c..46278857b1d 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/gitlab/config/resource/ProvisioningType.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/ProvisioningType.java
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.server.v2.api.gitlab.config.resource;
+package org.sonar.server.v2.api.model;
public enum ProvisioningType {
JIT, AUTO_PROVISIONING
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
index 7ec4a6387b8..7394832467b 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
@@ -24,6 +24,7 @@ import org.sonar.api.platform.Server;
import org.sonar.api.resources.Languages;
import org.sonar.db.Database;
import org.sonar.db.DbClient;
+import org.sonar.server.common.github.config.GithubConfigurationService;
import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.group.service.GroupService;
@@ -48,10 +49,10 @@ import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.analysis.controller.DefaultJresController;
+import org.sonar.server.v2.api.analysis.controller.DefaultScannerEngineController;
import org.sonar.server.v2.api.analysis.controller.DefaultVersionController;
import org.sonar.server.v2.api.analysis.controller.JresController;
import org.sonar.server.v2.api.analysis.controller.ScannerEngineController;
-import org.sonar.server.v2.api.analysis.controller.DefaultScannerEngineController;
import org.sonar.server.v2.api.analysis.controller.VersionController;
import org.sonar.server.v2.api.analysis.service.JresHandler;
import org.sonar.server.v2.api.analysis.service.JresHandlerImpl;
@@ -59,6 +60,8 @@ import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler;
import org.sonar.server.v2.api.analysis.service.ScannerEngineHandlerImpl;
import org.sonar.server.v2.api.dop.controller.DefaultDopSettingsController;
import org.sonar.server.v2.api.dop.controller.DopSettingsController;
+import org.sonar.server.v2.api.github.config.controller.DefaultGithubConfigurationController;
+import org.sonar.server.v2.api.github.config.controller.GithubConfigurationController;
import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController;
import org.sonar.server.v2.api.gitlab.config.controller.GitlabConfigurationController;
import org.sonar.server.v2.api.group.controller.DefaultGroupController;
@@ -159,6 +162,11 @@ public class PlatformLevel4WebConfig {
}
@Bean
+ public GithubConfigurationController githubConfigurationController(UserSession userSession, GithubConfigurationService githubConfigurationService) {
+ return new DefaultGithubConfigurationController(userSession, githubConfigurationService);
+ }
+
+ @Bean
public BoundProjectsController importedProjectsController(UserSession userSession, ImportProjectService importProjectService) {
return new DefaultBoundProjectsController(userSession, importProjectService);
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/config/DefaultGithubConfigurationControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/config/DefaultGithubConfigurationControllerTest.java
new file mode 100644
index 00000000000..0153e73f38a
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/config/DefaultGithubConfigurationControllerTest.java
@@ -0,0 +1,499 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.api.github.config;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.common.NonNullUpdatedValue;
+import org.sonar.server.common.github.config.GithubConfiguration;
+import org.sonar.server.common.github.config.GithubConfigurationService;
+import org.sonar.server.common.github.config.UpdateGithubConfigurationRequest;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.github.config.controller.DefaultGithubConfigurationController;
+import org.sonar.server.v2.api.github.config.resource.GithubConfigurationResource;
+import org.sonar.server.v2.api.github.config.response.GithubConfigurationSearchRestResponse;
+import org.sonar.server.v2.api.model.ProvisioningType;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.AUTO_PROVISIONING;
+import static org.sonar.server.common.gitlab.config.ProvisioningType.JIT;
+import static org.sonar.server.v2.WebApiEndpoints.GITHUB_CONFIGURATION_ENDPOINT;
+import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultGithubConfigurationControllerTest {
+ private static final Gson GSON = new GsonBuilder().create();
+
+ private static final GithubConfiguration GITHUB_CONFIGURATION = new GithubConfiguration(
+ "existing-id",
+ true,
+ "client-id",
+ "client-secret",
+ "application-id",
+ "private-key",
+ true,
+ "api.url.com",
+ "www.url.com",
+ Set.of("org1", "org2"),
+ AUTO_PROVISIONING,
+ true,
+ true,
+ true
+ );
+
+ private static final GithubConfigurationResource EXPECTED_GITHUB_CONF_RESOURCE = new GithubConfigurationResource(
+ GITHUB_CONFIGURATION.id(),
+ GITHUB_CONFIGURATION.enabled(),
+ GITHUB_CONFIGURATION.applicationId(),
+ GITHUB_CONFIGURATION.synchronizeGroups(),
+ GITHUB_CONFIGURATION.apiUrl(),
+ GITHUB_CONFIGURATION.webUrl(),
+ List.of("org1", "org2"),
+ ProvisioningType.valueOf(GITHUB_CONFIGURATION.provisioningType().name()),
+ GITHUB_CONFIGURATION.allowUsersToSignUp(),
+ GITHUB_CONFIGURATION.provisionProjectVisibility(),
+ GITHUB_CONFIGURATION.userConsentRequiredAfterUpgrade(),
+ "error-message");
+
+ private static final String EXPECTED_CONFIGURATION = """
+ {
+ "id": "existing-id",
+ "enabled": true,
+ "applicationId": "application-id",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [
+ "org1",
+ "org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": true,
+ "projectVisibility": true,
+ "userConsentRequiredAfterUpgrade": true,
+ "errorMessage": "error-message"
+ }
+ """;
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ private final GithubConfigurationService githubConfigurationService = mock();
+ private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultGithubConfigurationController(userSession, githubConfigurationService));
+
+ @Before
+ public void setUp() {
+ when(githubConfigurationService.validate(any())).thenReturn(Optional.of("error-message"));
+ }
+
+ @Test
+ public void fetchConfiguration_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(get(GITHUB_CONFIGURATION_ENDPOINT + "/1"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void fetchConfiguration_whenConfigNotFound_throws() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.getConfiguration("not-existing")).thenThrow(new NotFoundException("bla"));
+
+ mockMvc.perform(get(GITHUB_CONFIGURATION_ENDPOINT + "/not-existing"))
+ .andExpectAll(
+ status().isNotFound(),
+ content().json("{\"message\":\"bla\"}"));
+ }
+
+ @Test
+ public void fetchConfiguration_whenConfigFound_returnsIt() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.getConfiguration("existing-id")).thenReturn(GITHUB_CONFIGURATION);
+
+ mockMvc.perform(get(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id"))
+ .andExpectAll(
+ status().isOk(),
+ content().json(EXPECTED_CONFIGURATION));
+ }
+
+ @Test
+ public void search_whenNoParameters_shouldUseDefaultAndForwardToGroupMembershipService() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.findConfigurations()).thenReturn(Optional.of(GITHUB_CONFIGURATION));
+
+ MvcResult mvcResult = mockMvc.perform(get(GITHUB_CONFIGURATION_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ GithubConfigurationSearchRestResponse response = GSON.fromJson(mvcResult.getResponse().getContentAsString(), GithubConfigurationSearchRestResponse.class);
+
+ assertThat(response.page().pageSize()).isEqualTo(1000);
+ assertThat(response.page().pageIndex()).isEqualTo(1);
+ assertThat(response.page().total()).isEqualTo(1);
+ assertThat(response.githubConfigurations()).containsExactly(EXPECTED_GITHUB_CONF_RESOURCE);
+ }
+
+ @Test
+ public void search_whenNoParametersAndNoConfig_shouldReturnEmptyList() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.findConfigurations()).thenReturn(Optional.empty());
+
+ MvcResult mvcResult = mockMvc.perform(get(GITHUB_CONFIGURATION_ENDPOINT))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ GithubConfigurationSearchRestResponse response = GSON.fromJson(mvcResult.getResponse().getContentAsString(), GithubConfigurationSearchRestResponse.class);
+
+ assertThat(response.page().pageSize()).isEqualTo(1000);
+ assertThat(response.page().pageIndex()).isEqualTo(1);
+ assertThat(response.page().total()).isZero();
+ assertThat(response.githubConfigurations()).isEmpty();
+ }
+
+ @Test
+ public void updateConfiguration_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(patch(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content("{}"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void updateConfiguration_whenAllFieldsUpdated_performUpdates() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.updateConfiguration(any())).thenReturn(GITHUB_CONFIGURATION);
+
+ String payload = """
+ {
+ "enabled": true,
+ "clientId": "new-client-id",
+ "clientSecret": "new-client-secret",
+ "applicationId": "new-application-id",
+ "privateKey": "new-private-key",
+ "synchronizeGroups": false,
+ "apiUrl": "new-api.url.com",
+ "webUrl": "new-www.url.com",
+ "allowedOrganizations": [
+ "new-org1",
+ "new-org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": false,
+ "projectVisibility": false,
+ "userConsentRequiredAfterUpgrade": false
+ }
+ """;
+
+ mockMvc.perform(patch(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content(payload))
+ .andExpectAll(
+ status().isOk(),
+ content().json(EXPECTED_CONFIGURATION));
+
+ verify(githubConfigurationService).updateConfiguration(new UpdateGithubConfigurationRequest(
+ "existing-id",
+ NonNullUpdatedValue.withValueOrThrow(true),
+ NonNullUpdatedValue.withValueOrThrow("new-client-id"),
+ NonNullUpdatedValue.withValueOrThrow("new-client-secret"),
+ NonNullUpdatedValue.withValueOrThrow("new-application-id"),
+ NonNullUpdatedValue.withValueOrThrow("new-private-key"),
+ NonNullUpdatedValue.withValueOrThrow(false),
+ NonNullUpdatedValue.withValueOrThrow("new-api.url.com"),
+ NonNullUpdatedValue.withValueOrThrow("new-www.url.com"),
+ NonNullUpdatedValue.withValueOrThrow(Set.of("new-org1", "new-org2")),
+ NonNullUpdatedValue.withValueOrThrow(AUTO_PROVISIONING),
+ NonNullUpdatedValue.withValueOrThrow(false),
+ NonNullUpdatedValue.withValueOrThrow(false),
+ NonNullUpdatedValue.withValueOrThrow(false)
+ ));
+ }
+
+ @Test
+ public void updateConfiguration_whenSomeFieldsUpdated_performUpdates() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.updateConfiguration(any())).thenReturn(GITHUB_CONFIGURATION);
+
+ String payload = """
+ {
+ "enabled": false,
+ "provisioningType": "JIT",
+ "allowUsersToSignUp": false
+ }
+ """;
+
+ mockMvc.perform(patch(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content(payload))
+ .andExpectAll(
+ status().isOk(),
+ content().json(EXPECTED_CONFIGURATION));
+
+ verify(githubConfigurationService).updateConfiguration(new UpdateGithubConfigurationRequest(
+ "existing-id",
+ NonNullUpdatedValue.withValueOrThrow(false),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.withValueOrThrow(JIT),
+ NonNullUpdatedValue.withValueOrThrow(false),
+ NonNullUpdatedValue.undefined(),
+ NonNullUpdatedValue.undefined()
+ ));
+ }
+
+ @Test
+ public void create_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(
+ post(GITHUB_CONFIGURATION_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content("""
+ {
+ "enabled": true,
+ "clientId": "new-client-id",
+ "clientSecret": "new-client-secret",
+ "applicationId": "new-application-id",
+ "privateKey": "new-private-key",
+ "synchronizeGroups": false,
+ "apiUrl": "new-api.url.com",
+ "webUrl": "new-www.url.com",
+ "allowedOrganizations": [
+ "new-org1",
+ "new-org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": false,
+ "projectVisibility": false,
+ "userConsentRequiredAfterUpgrade": false
+ }
+ """))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void create_whenConfigCreated_returnsIt() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.createConfiguration(any())).thenReturn(GITHUB_CONFIGURATION);
+
+ mockMvc.perform(
+ post(GITHUB_CONFIGURATION_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content("""
+ {
+ "enabled": true,
+ "clientId": "client-id",
+ "clientSecret": "client-secret",
+ "applicationId": "application-id",
+ "privateKey": "private-key",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [
+ "org1",
+ "org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": true,
+ "projectVisibility": true,
+ "userConsentRequiredAfterUpgrade": true
+ }
+ """))
+ .andExpectAll(
+ status().isOk(),
+ content().json("""
+ {
+ "enabled": true,
+ "applicationId": "application-id",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [
+ "org1",
+ "org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": true,
+ "projectVisibility": true,
+ "userConsentRequiredAfterUpgrade": true
+ }
+ """));
+
+ }
+ @Test
+ public void create_whenConfigCreatedWithoutOptionalParams_returnsIt() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(githubConfigurationService.createConfiguration(any())).thenReturn(new GithubConfiguration(
+ "existing-id",
+ true,
+ "client-id",
+ "client-secret",
+ "application-id",
+ "private-key",
+ true,
+ "api.url.com",
+ "www.url.com",
+ Set.of(),
+ AUTO_PROVISIONING,
+ false,
+ false,
+ false
+ ));
+
+ mockMvc.perform(
+ post(GITHUB_CONFIGURATION_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content("""
+ {
+ "enabled": true,
+ "clientId": "client-id",
+ "clientSecret": "client-secret",
+ "applicationId": "application-id",
+ "privateKey": "private-key",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [],
+ "provisioningType": "AUTO_PROVISIONING"
+ }
+ """))
+ .andExpectAll(
+ status().isOk(),
+ content().json("""
+ {
+ "id": "existing-id",
+ "enabled": true,
+ "applicationId": "application-id",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": false,
+ "projectVisibility": false,
+ "userConsentRequiredAfterUpgrade": false
+ }
+ """));
+
+ }
+
+ @Test
+ public void create_whenRequiredParameterIsMissing_shouldReturnBadRequest() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(
+ post(GITHUB_CONFIGURATION_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content("""
+ {
+ "enabled": true,
+ "clientId": "client-id",
+ "clientSecret": "client-secret",
+ "privateKey": "private-key",
+ "synchronizeGroups": true,
+ "apiUrl": "api.url.com",
+ "webUrl": "www.url.com",
+ "allowedOrganizations": [
+ "org1",
+ "org2"
+ ],
+ "provisioningType": "AUTO_PROVISIONING",
+ "allowUsersToSignUp": true,
+ "projectVisibility": true,
+ "userConsentRequiredAfterUpgrade": true
+ }
+ """))
+ .andExpectAll(
+ status().isBadRequest(),
+ content().json(
+ "{\"message\":\"Value {} for field applicationId was rejected. Error: must not be empty.\"}"));
+
+ }
+
+ @Test
+ public void delete_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(
+ delete(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void delete_whenConfigIsDeleted_returnsNoContent() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ mockMvc.perform(
+ delete(GITHUB_CONFIGURATION_ENDPOINT + "/existing-id"))
+ .andExpectAll(
+ status().isNoContent());
+
+ verify(githubConfigurationService).deleteConfiguration("existing-id");
+ }
+
+ @Test
+ public void delete_whenConfigNotFound_returnsNotFound() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ doThrow(new NotFoundException("Not found")).when(githubConfigurationService).deleteConfiguration("not-existing");
+
+ mockMvc.perform(
+ delete(GITHUB_CONFIGURATION_ENDPOINT + "/not-existing"))
+ .andExpectAll(
+ status().isNotFound(),
+ content().json("{\"message\":\"Not found\"}"));
+ }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java
index 26e1ae364f6..ea85b7e8b9f 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/gitlab/config/DefaultGitlabConfigurationControllerTest.java
@@ -38,6 +38,7 @@ import org.sonar.server.v2.api.ControllerTester;
import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController;
import org.sonar.server.v2.api.gitlab.config.resource.GitlabConfigurationResource;
import org.sonar.server.v2.api.gitlab.config.response.GitlabConfigurationSearchRestResponse;
+import org.sonar.server.v2.api.model.ProvisioningType;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -83,7 +84,7 @@ public class DefaultGitlabConfigurationControllerTest {
GITLAB_CONFIGURATION.synchronizeGroups(),
List.of("group1", "group2"),
GITLAB_CONFIGURATION.allowUsersToSignUp(),
- org.sonar.server.v2.api.gitlab.config.resource.ProvisioningType.valueOf(GITLAB_CONFIGURATION.provisioningType().name()),
+ ProvisioningType.valueOf(GITLAB_CONFIGURATION.provisioningType().name()),
!GITLAB_CONFIGURATION.provisioningToken().isEmpty(),
"error-message");
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
index e61fd9d52eb..dfd2961d6b6 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/setting/ws/SetActionIT.java
@@ -69,6 +69,9 @@ import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.groups.Tuple.tuple;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_API_URL;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_WEB_URL;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto;
import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto;
import static org.sonar.db.user.UserTesting.newUserDto;
@@ -1128,6 +1131,26 @@ public class SetActionIT {
.hasMessage(format("Setting '%s' can only be used in sonar.properties", settingKey));
}
+ @DataProvider
+ public static Object[][] forbiddenProperties() {
+ return new Object[][] {
+ {GITLAB_AUTH_URL},
+ {GITHUB_API_URL},
+ {GITHUB_WEB_URL},
+ };
+ }
+
+ @Test
+ @UseDataProvider("forbiddenProperties")
+ public void fail_when_setting_key_is_forbidden(String property) {
+ TestRequest testRequest = ws.newRequest()
+ .setParam("key", property)
+ .setParam("value", "value");
+ assertThatThrownBy(testRequest::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("For security reasons, the key '%s' cannot be updated using this webservice. Please use the API v2", property);
+ }
+
@Test
public void fail_when_setting_key_is_forbidden() {
TestRequest testRequest = ws.newRequest()
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SetAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SetAction.java
index 034a2478dcd..555bf2d4062 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SetAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SetAction.java
@@ -57,6 +57,9 @@ import org.sonar.server.user.UserSession;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_API_URL;
+import static org.sonar.auth.github.GitHubSettings.GITHUB_WEB_URL;
+import static org.sonar.auth.gitlab.GitLabSettings.GITLAB_AUTH_URL;
import static org.sonar.server.exceptions.BadRequestException.checkRequest;
import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_FIELD_VALUES;
@@ -70,7 +73,7 @@ public class SetAction implements SettingsWsAction {
private static final String MSG_NO_EMPTY_VALUE = "A non empty value must be provided";
private static final int VALUE_MAXIMUM_LENGTH = 4000;
private static final TypeToken<Map<String, String>> MAP_TYPE_TOKEN = new TypeToken<>() {};
- private static final Set<String> FORBIDDEN_KEYS = Set.of("sonar.auth.gitlab.url");
+ private static final Set<String> FORBIDDEN_KEYS = Set.of(GITLAB_AUTH_URL, GITHUB_API_URL, GITHUB_WEB_URL);
private final PropertyDefinitions propertyDefinitions;
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 6ce6eb3bbe6..ac2c7151f1f 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -87,6 +87,7 @@ import org.sonar.server.common.almsettings.bitbucketserver.BitbucketServerProjec
import org.sonar.server.common.almsettings.github.GithubProjectCreatorFactory;
import org.sonar.server.common.almsettings.gitlab.GitlabProjectCreatorFactory;
import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.github.config.GithubConfigurationService;
import org.sonar.server.common.gitlab.config.GitlabConfigurationService;
import org.sonar.server.common.group.service.GroupMembershipService;
import org.sonar.server.common.group.service.GroupService;
@@ -404,6 +405,7 @@ public class PlatformLevel4 extends PlatformLevel {
new AuthenticationWsModule(),
new BitbucketModule(),
GitHubSettings.class,
+ GithubConfigurationService.class,
new GitHubModule(),
new GitLabModule(),
new LdapModule(),