diff options
author | Antoine Vigneau <antoine.vigneau@sonarsource.com> | 2024-06-11 17:44:45 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-06-17 20:02:35 +0000 |
commit | 078306d53ad53ba38d5d4b06e6e8958a0c2c6595 (patch) | |
tree | cd0ac74f560aac0e2a3720b096239d7519f856c0 /server | |
parent | 0bdfddeed0bf06255f61c6b59dcfc6d132598e14 (diff) | |
download | sonarqube-078306d53ad53ba38d5d4b06e6e8958a0c2c6595.tar.gz sonarqube-078306d53ad53ba38d5d4b06e6e8958a0c2c6595.zip |
SONAR-22365 Fix SSF-571
Diffstat (limited to 'server')
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(), |