From 639dc149dedc1456aa928d17457bec7f6a4ffbe5 Mon Sep 17 00:00:00 2001 From: Aurelien Poscia Date: Tue, 17 Oct 2023 08:55:05 +0200 Subject: [PATCH] SONAR-20700 Check project's permissions on GitHub in case auto-provisioning is on --- .../github/ImportGithubProjectActionIT.java | 5 +- .../ce/queue/BranchReportSubmitterIT.java | 2 +- .../server/ce/queue/ReportSubmitterIT.java | 53 +++- .../ws/DelegatingDevOpsPlatformService.java | 7 + .../almsettings/ws/DevOpsPlatformService.java | 2 + .../ws/GitHubDevOpsPlatformService.java | 90 ++++++- .../server/ce/queue/ReportSubmitter.java | 49 +++- .../DelegatingDevOpsPlatformServiceTest.java | 22 ++ .../ws/GitHubDevOpsPlatformServiceTest.java | 231 ++++++++++++++++-- .../platformlevel/PlatformLevel4.java | 2 + 10 files changed, 424 insertions(+), 39 deletions(-) diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java index 501c5bbc09a..9ebc0dce37c 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java @@ -31,6 +31,7 @@ import org.sonar.api.resources.Qualifiers; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.auth.github.GitHubSettings; +import org.sonar.auth.github.GithubPermissionConverter; import org.sonar.core.i18n.I18n; import org.sonar.core.platform.EditionProvider; import org.sonar.core.platform.PlatformEditionProvider; @@ -129,8 +130,10 @@ public class ImportGithubProjectActionIT { private final NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider); private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class); + + private final GithubPermissionConverter githubPermissionConverter = new GithubPermissionConverter(); private final GitHubDevOpsPlatformService gitHubDevOpsPlatformService = new GitHubDevOpsPlatformService(db.getDbClient(), - null, appClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings); + null, appClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings, githubPermissionConverter); private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), managedProjectService, userSession, componentUpdater, importHelper, newCodeDefinitionResolver, defaultBranchNameResolver, gitHubDevOpsPlatformService)); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java index 06da5572a64..4bcbd73524c 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java @@ -108,7 +108,7 @@ public class BranchReportSubmitterIT { private final BranchSupport branchSupport = spy(new BranchSupport(branchSupportDelegate)); private final DevOpsPlatformService devOpsPlatformService = new GitHubDevOpsPlatformService(db.getDbClient(), null, - null, projectDefaultVisibility, null, userSession, componentUpdater, null); + null, projectDefaultVisibility, null, userSession, componentUpdater, null, null); private final ManagedInstanceService managedInstanceService = mock(); private final ReportSubmitter underTest = new ReportSubmitter(queue, userSession, componentUpdater, permissionTemplateService, db.getDbClient(), branchSupport, diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java index 3a30c77763d..fd1cf3fb2e0 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java @@ -76,6 +76,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyMap; import static java.util.stream.IntStream.rangeClosed; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -123,7 +124,7 @@ public class ReportSubmitterIT { private final DevOpsPlatformService devOpsPlatformService = new DelegatingDevOpsPlatformService( Set.of(new GitHubDevOpsPlatformService(db.getDbClient(), githubGlobalSettingsValidator, - githubApplicationClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings))); + githubApplicationClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings, null))); private final DevOpsPlatformService devOpsPlatformServiceSpy = spy(devOpsPlatformService); @@ -275,7 +276,7 @@ public class ReportSubmitterIT { } @Test - public void submit_whenReportIsForANewProjectWithoutValidAlmSettings_createsProjectWithoutDevOpsBinding() { + public void submit_whenReportIsForANewGithubProjectWithoutValidAlmSettings_throws() { userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS); when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true); mockSuccessfulPrepareSubmitCall(); @@ -284,13 +285,18 @@ public class ReportSubmitterIT { DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, "apiUrl", "orga/repo"); when(devOpsPlatformServiceSpy.getDevOpsProjectDescriptor(characteristics)).thenReturn(Optional.of(projectDescriptor)); - underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8)); + assertThatIllegalArgumentException().isThrownBy(() -> underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8))) + .withMessage("The project orga/repo could not be created. It was auto-detected as a GITHUB project and no valid DevOps platform configuration were found to access apiUrl"); - assertLocalProjectWasCreated(); + assertNoProjectWasCreated(); + } + + private void assertNoProjectWasCreated() { + assertThat(db.getDbClient().projectDao().selectAll(db.getSession())).isEmpty(); } @Test - public void submit_whenReportIsForANewProjectWithValidAlmSettingsButAutoProvisioningOn_createsLocalProject() { + public void submit_whenReportIsForANewProjectWithValidAlmSettingsAutoProvisioningOnAndPermOnGh_createsProjectWithBinding() { userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS); when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true); when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true); @@ -299,10 +305,42 @@ public class ReportSubmitterIT { Map characteristics = Map.of("random", "data"); DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, "apiUrl", "orga/repo"); - mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(characteristics, projectDescriptor); + AlmSettingDto almSettingDto = mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(characteristics, projectDescriptor); + when(devOpsPlatformServiceSpy.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, projectDescriptor)).thenReturn(true); underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8)); + assertProjectWasCreatedWithBinding(); + } + + @Test + public void submit_whenReportIsForANewProjectWithProjectDescriptorAndNoValidAlmSettingsAndAutoProvisioningOn_throws() { + userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS); + when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true); + when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true); + mockSuccessfulPrepareSubmitCall(); + + Map characteristics = Map.of("random", "data"); + DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, "apiUrl", "orga/repo"); + doReturn(Optional.of(projectDescriptor)).when(devOpsPlatformServiceSpy).getDevOpsProjectDescriptor(characteristics); + + assertThatIllegalArgumentException() + .isThrownBy(() -> underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8))) + .withMessage("The project orga/repo could not be created. It was auto-detected as a GITHUB project and no valid DevOps platform configuration were found to access apiUrl"); + + assertNoProjectWasCreated(); + } + + @Test + public void submit_whenReportIsForANewProjectWithoutDevOpsMetadataAndAutoProvisioningOn_shouldCreateLocalProject() { + userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS); + when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true); + when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true); + mockSuccessfulPrepareSubmitCall(); + + Map characteristics = Map.of("random", "data"); + + underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8)); assertLocalProjectWasCreated(); } @@ -344,7 +382,7 @@ public class ReportSubmitterIT { assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.getUuid())).isPresent(); } - private void mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(Map characteristics, DevOpsProjectDescriptor projectDescriptor) { + private AlmSettingDto mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(Map characteristics, DevOpsProjectDescriptor projectDescriptor) { doReturn(Optional.of(projectDescriptor)).when(devOpsPlatformServiceSpy).getDevOpsProjectDescriptor(characteristics); AlmSettingDto almSettingDto = mock(AlmSettingDto.class); when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); @@ -352,6 +390,7 @@ public class ReportSubmitterIT { when(almSettingDto.getUuid()).thenReturn("TEST_GH"); doReturn(Optional.of(almSettingDto)).when(devOpsPlatformServiceSpy).getValidAlmSettingDto(any(), eq(projectDescriptor)); mockGithubInteractions(almSettingDto); + return almSettingDto; } private void mockGithubInteractions(AlmSettingDto almSettingDto) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java index dd764473d9f..608d8cf77d5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java @@ -59,6 +59,13 @@ public class DelegatingDevOpsPlatformService implements DevOpsPlatformService { .flatMap(delegate -> delegate.getValidAlmSettingDto(dbSession, devOpsProjectDescriptor)); } + @Override + public boolean isScanAllowedUsingPermissionsFromDevopsPlatform(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) { + return findDelegate(devOpsProjectDescriptor.alm()) + .map(delegate -> delegate.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, devOpsProjectDescriptor)) + .orElseThrow(() -> new IllegalStateException("No delegate found to handle projects on " + devOpsProjectDescriptor.alm())); + } + @Override public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java index fff70b8add4..6db06d88fd5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java @@ -35,6 +35,8 @@ public interface DevOpsPlatformService { Optional getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor); + boolean isScanAllowedUsingPermissionsFromDevopsPlatform(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor); + ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor); ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, AlmSettingDto almSettingDto, AccessToken accessToken, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java index 8ac5d5eb8b0..9e69daefbd0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java @@ -21,16 +21,23 @@ package org.sonar.server.almsettings.ws; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.alm.client.github.AppInstallationToken; import org.sonar.alm.client.github.GithubApplicationClient; import org.sonar.alm.client.github.GithubGlobalSettingsValidator; +import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; +import org.sonar.alm.client.github.api.GsonRepositoryTeam; import org.sonar.alm.client.github.config.GithubAppConfiguration; import org.sonar.alm.client.github.security.AccessToken; import org.sonar.api.server.ServerSide; +import org.sonar.api.web.UserRole; import org.sonar.auth.github.GitHubSettings; +import org.sonar.auth.github.GithubPermissionConverter; +import org.sonar.auth.github.GsonRepositoryPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.alm.setting.ALM; @@ -38,6 +45,8 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; import org.sonar.db.project.CreationMethod; import org.sonar.db.project.ProjectDto; +import org.sonar.db.provisioning.GithubPermissionsMappingDto; +import org.sonar.db.user.GroupDto; import org.sonar.server.almintegration.ws.ProjectKeyGenerator; import org.sonar.server.component.ComponentCreationData; import org.sonar.server.component.ComponentCreationParameters; @@ -70,10 +79,11 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService { private final UserSession userSession; private final ComponentUpdater componentUpdater; private final GitHubSettings gitHubSettings; + private final GithubPermissionConverter githubPermissionConverter; public GitHubDevOpsPlatformService(DbClient dbClient, GithubGlobalSettingsValidator githubGlobalSettingsValidator, GithubApplicationClient githubApplicationClient, ProjectDefaultVisibility projectDefaultVisibility, ProjectKeyGenerator projectKeyGenerator, UserSession userSession, - ComponentUpdater componentUpdater, GitHubSettings gitHubSettings) { + ComponentUpdater componentUpdater, GitHubSettings gitHubSettings, GithubPermissionConverter githubPermissionConverter) { this.dbClient = dbClient; this.githubGlobalSettingsValidator = githubGlobalSettingsValidator; this.githubApplicationClient = githubApplicationClient; @@ -82,6 +92,7 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService { this.userSession = userSession; this.componentUpdater = componentUpdater; this.gitHubSettings = gitHubSettings; + this.githubPermissionConverter = githubPermissionConverter; } @Override @@ -103,7 +114,7 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService { public Optional getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor) { Optional configurationToUse = dbClient.almSettingDao().selectByAlm(dbSession, getDevOpsPlatform()).stream() .filter(almSettingDto -> devOpsProjectDescriptor.url().equals(almSettingDto.getUrl())) - .filter(almSettingDto -> findInstallationIdToAccessRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier()).isPresent()) + .filter(almSettingDto -> findInstallationIdToAccessRepo(devOpsProjectDescriptor, almSettingDto)) .findFirst(); if (configurationToUse.isPresent()) { LOG.info("DevOps configuration {} auto-detected", configurationToUse.get().getKey()); @@ -114,11 +125,81 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService { return configurationToUse; } + private boolean findInstallationIdToAccessRepo(DevOpsProjectDescriptor devOpsProjectDescriptor, AlmSettingDto almSettingDto) { + GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto); + return findInstallationIdToAccessRepo(almSettingDto, githubAppConfiguration, devOpsProjectDescriptor.projectIdentifier()).isPresent(); + } + + @Override + public boolean isScanAllowedUsingPermissionsFromDevopsPlatform(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) { + GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto); + long installationId = findInstallationIdToAccessRepo(almSettingDto, githubAppConfiguration, devOpsProjectDescriptor.projectIdentifier()) + .orElseThrow(() -> new IllegalStateException(format("Impossible to find the repository %s on GitHub, using the devops config %s.", + devOpsProjectDescriptor.projectIdentifier(), almSettingDto.getKey()))); + return isScanAllowedUsingPermissionsFromGithub(devOpsProjectDescriptor.projectIdentifier(), githubAppConfiguration, installationId); + } + + private boolean isScanAllowedUsingPermissionsFromGithub(String organizationAndRepository, GithubAppConfiguration githubAppConfiguration, long installationId) { + AppInstallationToken accessToken = generateAppInstallationToken(githubAppConfiguration, installationId); + + String[] orgaAndRepoTokenified = organizationAndRepository.split("/"); + String organization = orgaAndRepoTokenified[0]; + String repository = orgaAndRepoTokenified[1]; + + Set permissionsMappingDtos = dbClient.githubPermissionsMappingDao().findAll(dbClient.openSession(false)); + + boolean userHasDirectAccessToRepo = doesUserHaveScanPermission(githubAppConfiguration.getApiEndpoint(), accessToken, organization, repository, permissionsMappingDtos); + if (userHasDirectAccessToRepo) { + return true; + } + return doesUserBelongToAGroupWithScanPermission(githubAppConfiguration.getApiEndpoint(), accessToken, organization, repository, permissionsMappingDtos); + } + + private boolean doesUserHaveScanPermission(String apiEndpoint, AppInstallationToken accessToken, String organization, String repository, + Set permissionsMappingDtos) { + Set repositoryCollaborators = githubApplicationClient.getRepositoryCollaborators(apiEndpoint, accessToken, organization, repository); + + String externalLogin = userSession.getExternalIdentity().map(UserSession.ExternalIdentity::login).orElse(null); + if (externalLogin == null) { + return false; + } + return repositoryCollaborators.stream() + .filter(gsonRepositoryCollaborator -> externalLogin.equals(gsonRepositoryCollaborator.name())) + .findAny() + .map(gsonRepositoryCollaborator -> hasScanPermission(permissionsMappingDtos, gsonRepositoryCollaborator.roleName(), gsonRepositoryCollaborator.permissions())) + .orElse(false); + } + + private boolean doesUserBelongToAGroupWithScanPermission(String apiUrl, AppInstallationToken accessToken, String organization, String repository, + Set permissionsMappingDtos) { + Set repositoryTeams = githubApplicationClient.getRepositoryTeams(apiUrl, accessToken, organization, repository); + + Set groupsOfUser = findUserMembershipOnSonarQube(organization); + return repositoryTeams.stream() + .filter(team -> hasScanPermission(permissionsMappingDtos, team.permission(), team.permissions())) + .map(GsonRepositoryTeam::name) + .anyMatch(groupsOfUser::contains); + } + + private Set findUserMembershipOnSonarQube(String organization) { + return userSession.getGroups().stream() + .map(GroupDto::getName) + .filter(groupName -> groupName.contains("/")) + .map(name -> name.replaceFirst(organization + "/", "")) + .collect(Collectors.toSet()); + } + + private boolean hasScanPermission(Set permissionsMappingDtos, String role, GsonRepositoryPermissions permissions) { + Set sonarqubePermissions = githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(permissionsMappingDtos, + role, permissions); + return sonarqubePermissions.contains(UserRole.SCAN); + } + @Override public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) { GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto); - GithubApplicationClient.Repository repository = findInstallationIdToAccessRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier()) + GithubApplicationClient.Repository repository = findInstallationIdToAccessRepo(almSettingDto, githubAppConfiguration, devOpsProjectDescriptor.projectIdentifier()) .flatMap(installationId -> findRepositoryOnGithub(devOpsProjectDescriptor.projectIdentifier(), githubAppConfiguration, installationId)) .orElseThrow(() -> new IllegalStateException(format("Impossible to find the repository %s on GitHub, using the devops config %s.", devOpsProjectDescriptor.projectIdentifier(), almSettingDto.getKey()))); @@ -126,9 +207,8 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService { return createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto, repository, SCANNER_API_DEVOPS_AUTO_CONFIG); } - private Optional findInstallationIdToAccessRepo(AlmSettingDto almSettingDto, String repositoryKey) { + private Optional findInstallationIdToAccessRepo(AlmSettingDto almSettingDto, GithubAppConfiguration githubAppConfiguration, String repositoryKey) { try { - GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto); return githubApplicationClient.getInstallationId(githubAppConfiguration, repositoryKey); } catch (Exception exception) { LOG.info(format("Could not use DevOps configuration '%s' to access repo %s. Error: %s", almSettingDto.getKey(), repositoryKey, exception.getMessage())); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java index 348c8a16157..9807ed236da 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; @@ -154,7 +155,7 @@ public class ReportSubmitter { // Project key is already used as a module of another project ComponentDto anotherBaseProject = dbClient.componentDao().selectOrFailByUuid(dbSession, component.branchUuid()); errors.add(format("The project '%s' is already defined in SonarQube but as a module of project '%s'. " - + "If you really want to stop directly analysing project '%s', please first delete it from SonarQube and then relaunch the analysis of project '%s'.", + + "If you really want to stop directly analysing project '%s', please first delete it from SonarQube and then relaunch the analysis of project '%s'.", rawProjectKey, anotherBaseProject.getKey(), anotherBaseProject.getKey(), rawProjectKey)); } if (!errors.isEmpty()) { @@ -166,21 +167,47 @@ public class ReportSubmitter { DbSession dbSession, BranchSupport.ComponentKey componentKey) { userSession.checkPermission(GlobalPermission.PROVISION_PROJECTS); - boolean wouldCurrentUserHaveScanPermission = permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(dbSession, userSession.getUuid(), projectKey); - if (!wouldCurrentUserHaveScanPermission) { - throw insufficientPrivilegesException(); + DevOpsProjectDescriptor devOpsProjectDescriptor = devOpsPlatformService.getDevOpsProjectDescriptor(characteristics).orElse(null); + AlmSettingDto almSettingDto = getAlmSettingDto(dbSession, devOpsProjectDescriptor); + + throwIfNoValidDevOpsConfigurationFoundForDevOpsProject(devOpsProjectDescriptor, almSettingDto); + throwIfCurrentUserWouldNotHaveScanPermission(projectKey, dbSession, devOpsProjectDescriptor, almSettingDto); + + if (almSettingDto != null) { + return devOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto, devOpsProjectDescriptor); } + return createProject(dbSession, componentKey.getKey(), defaultIfBlank(projectName, projectKey)); + } - if (managedInstanceService.isInstanceExternallyManaged()) { - return createProject(dbSession, componentKey.getKey(), defaultIfBlank(projectName, projectKey)); + @CheckForNull + private AlmSettingDto getAlmSettingDto(DbSession dbSession, @Nullable DevOpsProjectDescriptor devOpsProjectDescriptor) { + if (devOpsProjectDescriptor != null) { + return devOpsPlatformService.getValidAlmSettingDto(dbSession, devOpsProjectDescriptor).orElse(null); } + return null; + } - Optional devOpsProjectDescriptor = devOpsPlatformService.getDevOpsProjectDescriptor(characteristics); - Optional almSettingDto = devOpsProjectDescriptor.flatMap(descriptor -> devOpsPlatformService.getValidAlmSettingDto(dbSession, descriptor)); - if (almSettingDto.isPresent()) { - return devOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto.get(), devOpsProjectDescriptor.get()); + private static void throwIfNoValidDevOpsConfigurationFoundForDevOpsProject(@Nullable DevOpsProjectDescriptor devOpsProjectDescriptor, @Nullable AlmSettingDto almSettingDto) { + if (devOpsProjectDescriptor != null && almSettingDto == null) { + throw new IllegalArgumentException(format("The project %s could not be created. It was auto-detected as a %s project " + + "and no valid DevOps platform configuration were found to access %s", + devOpsProjectDescriptor.projectIdentifier(), devOpsProjectDescriptor.alm(), devOpsProjectDescriptor.url())); } - return createProject(dbSession, componentKey.getKey(), defaultIfBlank(projectName, projectKey)); + } + + private void throwIfCurrentUserWouldNotHaveScanPermission(String projectKey, DbSession dbSession, @Nullable DevOpsProjectDescriptor devOpsProjectDescriptor, + @Nullable AlmSettingDto almSettingDto) { + if (!wouldCurrentUserHaveScanPermission(projectKey, dbSession, devOpsProjectDescriptor, almSettingDto)) { + throw insufficientPrivilegesException(); + } + } + + private boolean wouldCurrentUserHaveScanPermission(String projectKey, DbSession dbSession, @Nullable DevOpsProjectDescriptor devOpsProjectDescriptor, + @Nullable AlmSettingDto almSettingDto) { + if (managedInstanceService.isInstanceExternallyManaged() && almSettingDto != null && devOpsProjectDescriptor != null) { + return devOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, devOpsProjectDescriptor); + } + return permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(dbSession, userSession.getUuid(), projectKey); } private ComponentCreationData createProject(DbSession dbSession, String projectKey, String projectName) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java index 0a7e7460cf3..cda5b3809e8 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java @@ -31,6 +31,7 @@ import org.sonar.db.alm.setting.AlmSettingDto; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -86,6 +87,26 @@ public class DelegatingDevOpsPlatformServiceTest { .usingRecursiveComparison().isEqualTo(new AlmSettingDto().setAlm(ALM.GITHUB)); } + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenNoDelegates_shouldThrow() { + DevOpsProjectDescriptor devOpsProjectDescriptor = mock(); + when(devOpsProjectDescriptor.alm()).thenReturn(ALM.GITHUB); + + assertThatIllegalStateException() + .isThrownBy(() -> NO_DEVOPS_PLATFORMS.isScanAllowedUsingPermissionsFromDevopsPlatform(mock(), devOpsProjectDescriptor)) + .withMessage("No delegate found to handle projects on GITHUB"); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenDelegates_shouldReturnDelegateResponse() { + DevOpsProjectDescriptor devOpsProjectDescriptor = mock(); + when(devOpsProjectDescriptor.alm()).thenReturn(ALM.GITHUB); + + boolean isScanAllowed = MULTIPLE_DEVOPS_PLATFORMS.isScanAllowedUsingPermissionsFromDevopsPlatform(mock(), devOpsProjectDescriptor); + + assertThat(isScanAllowed).isTrue(); + } + private static DevOpsPlatformService mockGitHubDevOpsPlatformService() { DevOpsPlatformService mockDevOpsPlatformService = mock(); when(mockDevOpsPlatformService.getDevOpsPlatform()).thenReturn(ALM.GITHUB); @@ -93,6 +114,7 @@ public class DelegatingDevOpsPlatformServiceTest { .thenReturn(Optional.of(new DevOpsProjectDescriptor(ALM.GITHUB, "githubUrl", "githubRepo"))); when(mockDevOpsPlatformService.getValidAlmSettingDto(any(), any())) .thenReturn(Optional.of(new AlmSettingDto().setAlm(ALM.GITHUB))); + when(mockDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(any(), any())).thenReturn(true); return mockDevOpsPlatformService; } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java index db7328ae407..0820c280f69 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java @@ -19,9 +19,11 @@ */ package org.sonar.server.almsettings.ws; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -35,11 +37,16 @@ import org.mockito.junit.MockitoJUnitRunner; import org.sonar.alm.client.github.AppInstallationToken; import org.sonar.alm.client.github.GithubApplicationClient; import org.sonar.alm.client.github.GithubGlobalSettingsValidator; +import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; +import org.sonar.alm.client.github.api.GsonRepositoryTeam; import org.sonar.alm.client.github.config.GithubAppConfiguration; import org.sonar.api.resources.Qualifiers; import org.sonar.api.testfixtures.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.api.web.UserRole; import org.sonar.auth.github.GitHubSettings; +import org.sonar.auth.github.GithubPermissionConverter; +import org.sonar.auth.github.GsonRepositoryPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.alm.setting.ALM; @@ -48,6 +55,8 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDao; import org.sonar.db.alm.setting.ProjectAlmSettingDto; import org.sonar.db.project.CreationMethod; import org.sonar.db.project.ProjectDto; +import org.sonar.db.provisioning.GithubPermissionsMappingDto; +import org.sonar.db.user.GroupDto; import org.sonar.server.almintegration.ws.ProjectKeyGenerator; import org.sonar.server.component.ComponentCreationData; import org.sonar.server.component.ComponentCreationParameters; @@ -58,6 +67,7 @@ import org.sonar.server.project.Visibility; import org.sonar.server.user.UserSession; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; @@ -84,6 +94,7 @@ public class GitHubDevOpsPlatformServiceTest { private static final String ORGANIZATION_NAME = "orgname"; private static final String GITHUB_REPO_FULL_NAME = ORGANIZATION_NAME + "/" + PROJECT_NAME; private static final String GITHUB_API_URL = "https://api.toto.com"; + private static final DevOpsProjectDescriptor DEV_OPS_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME); @Mock private DbSession dbSession; @@ -109,6 +120,9 @@ public class GitHubDevOpsPlatformServiceTest { @Mock private GitHubSettings gitHubSettings; + @Mock + private GithubPermissionConverter githubPermissionConverter; + @InjectMocks private GitHubDevOpsPlatformService gitHubDevOpsPlatformService; @@ -179,23 +193,20 @@ public class GitHubDevOpsPlatformServiceTest { @Test public void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() { - DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME); - AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor); + AlmSettingDto almSettingDto = mockAlmSettingDto(); GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto); when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.empty()); assertThatIllegalStateException().isThrownBy( - () -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, devOpsProjectDescriptor)) + () -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR)) .withMessage("Impossible to find the repository orgname/projectName on GitHub, using the devops config devops-platform-config-1."); } @Test public void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnGitHub_successfullyCreatesProject() { // given - DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME); - - AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor); + AlmSettingDto almSettingDto = mockAlmSettingDto(); mockExistingGitHubRepository(almSettingDto); ComponentCreationData componentCreationData = mockProjectCreation(); @@ -204,7 +215,7 @@ public class GitHubDevOpsPlatformServiceTest { // when ComponentCreationData actualComponentCreationData = gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, - devOpsProjectDescriptor); + DEV_OPS_PROJECT_DESCRIPTOR); // then assertThat(actualComponentCreationData).isEqualTo(componentCreationData); @@ -224,11 +235,10 @@ public class GitHubDevOpsPlatformServiceTest { @Test public void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnGitHubAndAutoProvisioningOn_successfullyCreatesProject() { // given - DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME); when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE); when(gitHubSettings.isProvisioningEnabled()).thenReturn(true); - AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor); + AlmSettingDto almSettingDto = mockAlmSettingDto(); mockExistingGitHubRepository(almSettingDto); ComponentCreationData componentCreationData = mockProjectCreation(); @@ -237,7 +247,7 @@ public class GitHubDevOpsPlatformServiceTest { // when ComponentCreationData actualComponentCreationData = gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, - devOpsProjectDescriptor); + DEV_OPS_PROJECT_DESCRIPTOR); // then assertThat(actualComponentCreationData).isEqualTo(componentCreationData); @@ -256,17 +266,210 @@ public class GitHubDevOpsPlatformServiceTest { @Test public void createProjectAndBindToDevOpsPlatform_whenWrongToken_throws() { - DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME); - AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor); + AlmSettingDto almSettingDto = mockAlmSettingDto(); mockExistingGitHubRepository(almSettingDto); when(githubApplicationClient.createAppInstallationToken(any(), anyLong())).thenReturn(Optional.empty()); assertThatIllegalStateException().isThrownBy( - () -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, devOpsProjectDescriptor)) + () -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR)) .withMessage("Error while generating token for GitHub Api Url https://api.toto.com (installation id: 534534534543)"); } + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsCollaboratorWithScan_returnsTrue() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1"); + GsonRepositoryCollaborator collaborator2 = mockCollaborator("collaborator2", 2, "role2"); + mockGithubCollaboratorsFromApi(almSettingDto, collaborator1, collaborator2); + bindSessionToCollaborator(collaborator2); + + mockPermissionsConversion(collaborator2, "another_perm", UserRole.SCAN); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isTrue(); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsCollaboratorButWithoutScan_returnsFalse() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1"); + mockGithubCollaboratorsFromApi(almSettingDto, collaborator1); + bindSessionToCollaborator(collaborator1); + + mockPermissionsConversion(collaborator1, "admin"); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isFalse(); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenRepoFoundOnGitHubAndUserIsCollaboratorButWithoutScan_returnsFalse() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1"); + mockGithubCollaboratorsFromApi(almSettingDto, collaborator1); + bindSessionToCollaborator(collaborator1); + + mockPermissionsConversion(collaborator1, "admin"); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isFalse(); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenRepoFoundOnGitHubAndUserIsNotExternal_returnsFalse() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1"); + mockGithubCollaboratorsFromApi(almSettingDto, collaborator1); + mockPermissionsConversion(collaborator1, "admin"); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isFalse(); + } + + private void mockPermissionsConversion(GsonRepositoryCollaborator collaborator, String... sqPermissions) { + Set githubPermissionsMappingDtos = mockPermissionsMappingsDtos(); + when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, collaborator.roleName(), collaborator.permissions())) + .thenReturn(Arrays.stream(sqPermissions).collect(toSet())); + } + + private void bindSessionToCollaborator(GsonRepositoryCollaborator collaborator1) { + UserSession.ExternalIdentity externalIdentity = new UserSession.ExternalIdentity(String.valueOf(collaborator1.id()), collaborator1.name()); + when(userSession.getExternalIdentity()).thenReturn(Optional.of(externalIdentity)); + } + + private static GsonRepositoryCollaborator mockCollaborator(String collaborator1, int id, String role1) { + return new GsonRepositoryCollaborator(collaborator1, id, role1, + new GsonRepositoryPermissions(false, false, false, false, false)); + } + + private void mockGithubCollaboratorsFromApi(AlmSettingDto almSettingDto, GsonRepositoryCollaborator... repositoryCollaborators) { + GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto); + when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.of(APP_INSTALLATION_ID)); + AppInstallationToken appInstallationToken = mockAppInstallationToken(githubAppConfiguration, APP_INSTALLATION_ID); + when(githubApplicationClient.getRepositoryCollaborators(GITHUB_API_URL, appInstallationToken, ORGANIZATION_NAME, PROJECT_NAME)) + .thenReturn(Arrays.stream(repositoryCollaborators).collect(toSet())); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenInstallationIdNotFound_throws() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + when(githubApplicationClient.getInstallationId(any(), any())).thenReturn(Optional.empty()); + + // when + assertThatIllegalStateException() + .isThrownBy(() -> gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR)) + .withMessage("Impossible to find the repository orgname/projectName on GitHub, using the devops config devops-platform-config-1."); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsMemberOfAGroupWithScan_returnsTrue() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1"); + GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2"); + mockTeamsFromApi(almSettingDto, team1, team2); + bindGroupsToUser(team1.name(), team2.name()); + + mockPermissionsConversion(team1, "another_perm", UserRole.SCAN); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isTrue(); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsMemberOfAGroupWithoutScanPermission_returnsFalse() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1"); + mockTeamsFromApi(almSettingDto, team1); + bindGroupsToUser(team1.name(), "another_local_team"); + + mockPermissionsConversion(team1, "another_perm", "admin"); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isFalse(); + } + + @Test + public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsNotMemberOfAGroupWithScanPermission_returnsFalse() { + // given + AlmSettingDto almSettingDto = mockAlmSettingDto(); + + GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1"); + mockTeamsFromApi(almSettingDto, team1); + bindGroupsToUser("another_local_team"); + + mockPermissionsConversion(team1, "another_perm", UserRole.SCAN); + + // when + boolean isGranted = gitHubDevOpsPlatformService.isScanAllowedUsingPermissionsFromDevopsPlatform(almSettingDto, DEV_OPS_PROJECT_DESCRIPTOR); + + // then + assertThat(isGranted).isFalse(); + } + + private void bindGroupsToUser(String... groupNames) { + Set groupDtos = Arrays.stream(groupNames) + .map(groupName -> new GroupDto().setName(ORGANIZATION_NAME + "/" + groupName).setUuid("uuid_" + groupName)) + .collect(toSet()); + when(userSession.getGroups()).thenReturn(groupDtos); + } + + private static GsonRepositoryTeam mockGithubTeam(String name, int id, String role) { + return new GsonRepositoryTeam(name, id, name + "slug", role, new GsonRepositoryPermissions(false, false, false, false, false)); + } + + private void mockTeamsFromApi(AlmSettingDto almSettingDto, GsonRepositoryTeam... repositoryTeams) { + GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto); + when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.of(APP_INSTALLATION_ID)); + AppInstallationToken appInstallationToken = mockAppInstallationToken(githubAppConfiguration, APP_INSTALLATION_ID); + when(githubApplicationClient.getRepositoryTeams(GITHUB_API_URL, appInstallationToken, ORGANIZATION_NAME, PROJECT_NAME)) + .thenReturn(Arrays.stream(repositoryTeams).collect(toSet())); + } + + private void mockPermissionsConversion(GsonRepositoryTeam team, String... sqPermissions) { + Set githubPermissionsMappingDtos = mockPermissionsMappingsDtos(); + when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, team.permission(), team.permissions())) + .thenReturn(Arrays.stream(sqPermissions).collect(toSet())); + } + + private Set mockPermissionsMappingsDtos() { + Set githubPermissionsMappingDtos = Set.of(mock(GithubPermissionsMappingDto.class)); + when(dbClient.githubPermissionsMappingDao().findAll(any())).thenReturn(githubPermissionsMappingDtos); + return githubPermissionsMappingDtos; + } + private void mockExistingGitHubRepository(AlmSettingDto almSettingDto) { GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto); when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.of(APP_INSTALLATION_ID)); @@ -296,7 +499,7 @@ public class GitHubDevOpsPlatformServiceTest { return appInstallationToken; } - private static AlmSettingDto mockAlmSettingDto(DevOpsProjectDescriptor devOpsProjectDescriptor) { + private static AlmSettingDto mockAlmSettingDto() { AlmSettingDto almSettingDto = mock(); when(almSettingDto.getUuid()).thenReturn("almsetting-uuid-1"); when(almSettingDto.getKey()).thenReturn("devops-platform-config-1"); 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 097e0d363d5..4f9c29e96b3 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 @@ -41,6 +41,7 @@ import org.sonar.api.server.rule.RulesDefinitionXmlLoader; import org.sonar.auth.bitbucket.BitbucketModule; import org.sonar.auth.github.GitHubModule; import org.sonar.auth.github.GitHubSettings; +import org.sonar.auth.github.GithubPermissionConverter; import org.sonar.auth.gitlab.GitLabModule; import org.sonar.auth.ldap.LdapModule; import org.sonar.auth.saml.SamlModule; @@ -552,6 +553,7 @@ public class PlatformLevel4 extends PlatformLevel { GithubProvisioningConfigValidator.class, GithubProvisioningWs.class, GitHubDevOpsPlatformService.class, + GithubPermissionConverter.class, BitbucketCloudRestClientConfiguration.class, BitbucketServerRestClient.class, GitlabHttpClient.class, -- 2.39.5