From: Aurelien Poscia Date: Mon, 7 Aug 2023 15:12:45 +0000 (+0200) Subject: SONAR-19789 schedule permissions sync task upon project onboarding X-Git-Tag: 10.2.0.77647~246 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8a7995fb4e79c7fd5b0a22401fcaabb2db6c5a06;p=sonarqube.git SONAR-19789 schedule permissions sync task upon project onboarding --- diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskTypes.java b/server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskTypes.java index 42c7f85ee96..356139f405b 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskTypes.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskTypes.java @@ -26,6 +26,8 @@ public final class CeTaskTypes { public static final String REPORT = "REPORT"; public static final String PROJECT_EXPORT = "PROJECT_EXPORT"; + public static final String GITHUB_PROJECT_PERMISSIONS_PROVISIONING = "GITHUB_PROJECT_PERMISSIONS_PROVISIONING"; + private CeTaskTypes() { // only statics } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java b/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java index 2a76ad5779c..26d0f049341 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java @@ -124,6 +124,12 @@ public class DelegatingManagedServices implements ManagedInstanceService, Manage .orElse(false); } + @Override + public void queuePermissionSyncTask(String submitterUuid, String componentUuid, String projectUuid) { + findManagedProjectService() + .ifPresent(managedProjectService -> managedProjectService.queuePermissionSyncTask(submitterUuid, componentUuid, projectUuid)); + } + private Optional findManagedProjectService() { return findManagedInstanceService() .filter(ManagedProjectService.class::isInstance) diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java b/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java index 4679195554e..9b4c3b88b80 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java @@ -29,4 +29,5 @@ public interface ManagedProjectService { boolean isProjectManaged(DbSession dbSession, String projectUuid); + void queuePermissionSyncTask(String submitterUuid, String componentUuid, String projectUuid); } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/project/VisibilityService.java b/server/sonar-server-common/src/main/java/org/sonar/server/project/VisibilityService.java index 8768b1d5625..2e4eccf5f87 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/project/VisibilityService.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/project/VisibilityService.java @@ -19,6 +19,7 @@ */ package org.sonar.server.project; +import com.google.common.annotations.VisibleForTesting; import java.util.Optional; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.server.ServerSide; @@ -38,6 +39,7 @@ import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.sonar.api.utils.Preconditions.checkState; import static org.sonar.api.web.UserRole.PUBLIC_PERMISSIONS; +import static org.sonar.db.ce.CeTaskTypes.GITHUB_PROJECT_PERMISSIONS_PROVISIONING; @ServerSide @ComputeEngineSide @@ -66,14 +68,19 @@ public class VisibilityService { } } } - private void checkNoPendingTasks(DbSession dbSession, EntityDto entityDto) { - //This check likely can be removed when we remove the column 'private' from components table - checkState(noPendingTask(dbSession, entityDto.getKey()), "Component visibility can't be changed as long as it has background task(s) pending or in progress"); + + @VisibleForTesting + void checkNoPendingTasks(DbSession dbSession, EntityDto entityDto) { + //This check likely can be removed when we remove the column 'private' from components table in SONAR-20126. + checkState(countPendingTask(dbSession, entityDto.getKey()) == 0, "Component visibility can't be changed as long as it has background task(s) pending or in progress"); } - private boolean noPendingTask(DbSession dbSession, String entityKey) { + private long countPendingTask(DbSession dbSession, String entityKey) { EntityDto entityDto = dbClient.entityDao().selectByKey(dbSession, entityKey).orElseThrow(() -> new IllegalStateException("Can't find entity " + entityKey)); - return dbClient.ceQueueDao().selectByEntityUuid(dbSession, entityDto.getUuid()).isEmpty(); + return dbClient.ceQueueDao().selectByEntityUuid(dbSession, entityDto.getUuid()) + .stream() + .filter(task -> !task.getTaskType().equals(GITHUB_PROJECT_PERMISSIONS_PROVISIONING)) + .count(); } private void setPrivateForRootComponentUuid(DbSession dbSession, EntityDto entity, boolean newIsPrivate) { diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java index 44ee3c5c674..92e29cb833c 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java @@ -30,9 +30,14 @@ import org.sonar.db.DbSession; 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.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @@ -46,9 +51,7 @@ public class DelegatingManagedServicesTest { @Test public void getProviderName_whenNotManaged_shouldThrow() { - DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES; - - assertThatThrownBy(managedInstanceService::getProviderName) + assertThatThrownBy(NO_MANAGED_SERVICES::getProviderName) .isInstanceOf(IllegalStateException.class) .hasMessage("This instance is not managed."); } @@ -62,8 +65,7 @@ public class DelegatingManagedServicesTest { @Test public void isInstanceExternallyManaged_whenNoManagedInstanceService_returnsFalse() { - DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES; - assertThat(managedInstanceService.isInstanceExternallyManaged()).isFalse(); + assertThat(NO_MANAGED_SERVICES.isInstanceExternallyManaged()).isFalse(); } @Test @@ -85,9 +87,8 @@ public class DelegatingManagedServicesTest { @Test public void getUserUuidToManaged_whenNoDelegates_setAllUsersAsNonManaged() { Set userUuids = Set.of("a", "b"); - DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES; - Map userUuidToManaged = managedInstanceService.getUserUuidToManaged(dbSession, userUuids); + Map userUuidToManaged = NO_MANAGED_SERVICES.getUserUuidToManaged(dbSession, userUuids); assertThat(userUuidToManaged).containsExactlyInAnyOrderEntriesOf(Map.of("a", false, "b", false)); } @@ -257,9 +258,24 @@ public class DelegatingManagedServicesTest { @Test public void isProjectManaged_whenManagedNoInstanceServices_returnsFalse() { - DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES; + assertThat(NO_MANAGED_SERVICES.isProjectManaged(dbSession, "whatever")).isFalse(); + } + + @Test + public void queuePermissionSyncTask_whenManagedNoInstanceServices_doesNotFail() { + assertThatNoException().isThrownBy(() -> NO_MANAGED_SERVICES.queuePermissionSyncTask("userUuid", "componentUuid", "projectUuid")); + } - assertThat(managedInstanceService.isProjectManaged(dbSession, "whatever")).isFalse(); + @Test + public void queuePermissionSyncTask_whenManagedInstanceServices_shouldDelegatesToRightService() { + NeverManagedInstanceService neverManagedInstanceService = spy(new NeverManagedInstanceService()); + AlwaysManagedInstanceService alwaysManagedInstanceService = spy(new AlwaysManagedInstanceService()); + Set delegates = Set.of(neverManagedInstanceService, alwaysManagedInstanceService); + DelegatingManagedServices managedInstanceService = new DelegatingManagedServices(delegates); + + managedInstanceService.queuePermissionSyncTask("userUuid", "componentUuid", "projectUuid"); + verify(neverManagedInstanceService, never()).queuePermissionSyncTask(anyString(), anyString(), anyString()); + verify(alwaysManagedInstanceService).queuePermissionSyncTask("userUuid", "componentUuid", "projectUuid"); } private static class NeverManagedInstanceService implements ManagedInstanceService, ManagedProjectService { @@ -313,6 +329,11 @@ public class DelegatingManagedServicesTest { public boolean isProjectManaged(DbSession dbSession, String projectUuid) { return false; } + + @Override + public void queuePermissionSyncTask(String submitterUuid, String componentUuid, String projectUuid) { + + } } private static class AlwaysManagedInstanceService implements ManagedInstanceService, ManagedProjectService { @@ -366,6 +387,11 @@ public class DelegatingManagedServicesTest { public boolean isProjectManaged(DbSession dbSession, String projectUuid) { return true; } + + @Override + public void queuePermissionSyncTask(String submitterUuid, String componentUuid, String projectUuid) { + + } } } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/project/VisibilityServiceTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/project/VisibilityServiceTest.java new file mode 100644 index 00000000000..d31a8658038 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/project/VisibilityServiceTest.java @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.project; + +import java.util.List; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.db.entity.EntityDto; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.db.ce.CeTaskTypes.GITHUB_PROJECT_PERMISSIONS_PROVISIONING; + +@RunWith(MockitoJUnitRunner.class) +public class VisibilityServiceTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private DbClient dbClient; + + @Mock + private DbSession dbSession; + + @InjectMocks + private VisibilityService visibilityService; + + @Test + public void checkNoPendingTasks_whenEntityNotFound_throwsIae() { + EntityDto entityDto = mockEntityDto(); + when(dbClient.entityDao().selectByKey(dbSession, entityDto.getKey())).thenReturn(Optional.empty()); + + assertThatIllegalStateException() + .isThrownBy(() -> visibilityService.checkNoPendingTasks(dbSession, entityDto)) + .withMessage("Can't find entity entityKey"); + } + + @Test + public void checkNoPendingTasks_whenEntityFoundAndNoTaskInQueue_doesNotThrow() { + EntityDto entityDto = mockEntityDto(); + when(dbClient.entityDao().selectByKey(dbSession, entityDto.getKey())).thenReturn(Optional.of(entityDto)); + + visibilityService.checkNoPendingTasks(dbSession, entityDto); + } + + @Test + public void checkNoPendingTasks_whenOneGithubSyncTaskInQueue_doesNotThrow() { + EntityDto entityDto = mockEntityDto(); + when(dbClient.entityDao().selectByKey(dbSession, entityDto.getKey())).thenReturn(Optional.of(entityDto)); + + mockCeQueueDto(GITHUB_PROJECT_PERMISSIONS_PROVISIONING, entityDto.getKey()); + + visibilityService.checkNoPendingTasks(dbSession, entityDto); + } + + @Test + public void checkNoPendingTasks_whenAnyOtherTaskInQueue_throws() { + EntityDto entityDto = mockEntityDto(); + when(dbClient.entityDao().selectByKey(dbSession, entityDto.getKey())).thenReturn(Optional.of(entityDto)); + + mockCeQueueDto("ANYTHING", entityDto.getUuid()); + + assertThatIllegalStateException() + .isThrownBy(() -> visibilityService.checkNoPendingTasks(dbSession, entityDto)) + .withMessage("Component visibility can't be changed as long as it has background task(s) pending or in progress"); + } + + private void mockCeQueueDto(String taskType, String entityDto) { + CeQueueDto ceQueueDto = mock(CeQueueDto.class); + when(ceQueueDto.getTaskType()).thenReturn(taskType); + when(dbClient.ceQueueDao().selectByEntityUuid(dbSession, entityDto)).thenReturn(List.of(ceQueueDto)); + } + + private static EntityDto mockEntityDto() { + EntityDto entityDto = mock(EntityDto.class); + when(entityDto.getKey()).thenReturn("entityKey"); + when(entityDto.getUuid()).thenReturn("entityUuid"); + return entityDto; + } +} 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 140d53a9c91..5552328191c 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 @@ -54,6 +54,7 @@ import org.sonar.server.es.TestIndexers; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.favorite.FavoriteUpdater; +import org.sonar.server.management.ManagedProjectService; import org.sonar.server.newcodeperiod.NewCodeDefinitionResolver; import org.sonar.server.permission.GroupPermissionChanger; import org.sonar.server.permission.PermissionService; @@ -123,7 +124,8 @@ public class ImportGithubProjectActionIT { private final GitHubSettings gitHubSettings = mock(GitHubSettings.class); private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider); - private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), userSession, + private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class); + private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), managedProjectService, userSession, projectDefaultVisibility, appClient, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver, defaultBranchNameResolver, gitHubSettings)); @@ -161,6 +163,7 @@ public class ImportGithubProjectActionIT { assertThat(mainBranch).isPresent(); assertThat(mainBranch.get().getKey()).isEqualTo("default-branch"); + verify(managedProjectService).queuePermissionSyncTask(userSession.getUuid(), mainBranch.get().getUuid() , projectDto.get().getUuid()); } @Test diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java index f59b4cf84c6..41e77913588 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java @@ -19,6 +19,7 @@ */ package org.sonar.server.almintegration.ws.github; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; import org.sonar.alm.client.github.GithubApplicationClient; @@ -44,6 +45,7 @@ import org.sonar.server.component.ComponentCreationData; import org.sonar.server.component.ComponentUpdater; import org.sonar.server.component.NewComponent; import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.management.ManagedProjectService; import org.sonar.server.newcodeperiod.NewCodeDefinitionResolver; import org.sonar.server.project.DefaultBranchNameResolver; import org.sonar.server.project.ProjectDefaultVisibility; @@ -68,6 +70,8 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction { public static final String PARAM_REPOSITORY_KEY = "repositoryKey"; private final DbClient dbClient; + + private final ManagedProjectService managedProjectService; private final UserSession userSession; private final ProjectDefaultVisibility projectDefaultVisibility; private final GithubApplicationClient githubApplicationClient; @@ -82,11 +86,12 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction { private final GitHubSettings gitHubSettings; @Inject - public ImportGithubProjectAction(DbClient dbClient, UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility, + public ImportGithubProjectAction(DbClient dbClient, ManagedProjectService managedProjectService, UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility, GithubApplicationClientImpl githubApplicationClient, ComponentUpdater componentUpdater, ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator, NewCodeDefinitionResolver newCodeDefinitionResolver, DefaultBranchNameResolver defaultBranchNameResolver, GitHubSettings gitHubSettings) { this.dbClient = dbClient; + this.managedProjectService = managedProjectService; this.userSession = userSession; this.projectDefaultVisibility = projectDefaultVisibility; this.githubApplicationClient = githubApplicationClient; @@ -172,6 +177,9 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction { componentUpdater.commitAndIndex(dbSession, componentCreationData); + String userUuid = Objects.requireNonNull(userSession.getUuid()); + managedProjectService.queuePermissionSyncTask(userUuid, mainBranchDto.getUuid(), projectDto.getUuid()); + return toCreateResponse(projectDto); } } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 737a5a5e0ba..a19a7f5acf5 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3367,6 +3367,7 @@ background_task.type.PROJECT_IMPORT=Project Import background_task.type.AUDIT_PURGE=Audit Log Purge background_task.type.REPORT_SUBMIT=Report Email Submit background_task.type.GITHUB_AUTH_PROVISIONING=Github Provisioning +background_task.type.GITHUB_PROJECT_PERMISSIONS_PROVISIONING=Github project permission sync background_tasks.page=Background Tasks background_tasks.page.description=This page allows monitoring of the queue of tasks running asynchronously on the server. It also gives access to the history of finished tasks and their status. Analysis report processing is the most common kind of background task.