]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19789 schedule permissions sync task upon project onboarding
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Mon, 7 Aug 2023 15:12:45 +0000 (17:12 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 9 Aug 2023 20:03:37 +0000 (20:03 +0000)
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskTypes.java
server/sonar-server-common/src/main/java/org/sonar/server/management/DelegatingManagedServices.java
server/sonar-server-common/src/main/java/org/sonar/server/management/ManagedProjectService.java
server/sonar-server-common/src/main/java/org/sonar/server/project/VisibilityService.java
server/sonar-server-common/src/test/java/org/sonar/server/management/DelegatingManagedServicesTest.java
server/sonar-server-common/src/test/java/org/sonar/server/project/VisibilityServiceTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 42c7f85ee96ca24939b523c6b7dfb39f3a5a41d1..356139f405b23ade0254a5449ad284110db8a2e0 100644 (file)
@@ -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
   }
index 2a76ad5779ceab24be8fa2c0742b61f81cf36944..26d0f0493419adcadba2049eb8b96526eb9c0d5f 100644 (file)
@@ -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<ManagedProjectService> findManagedProjectService() {
     return findManagedInstanceService()
       .filter(ManagedProjectService.class::isInstance)
index 4679195554e66b2745cca4aac59349845308cf0d..9b4c3b88b80e1d0b95002a3dcbb055205ad1a58b 100644 (file)
@@ -29,4 +29,5 @@ public interface ManagedProjectService {
 
   boolean isProjectManaged(DbSession dbSession, String projectUuid);
 
+  void queuePermissionSyncTask(String submitterUuid, String componentUuid, String projectUuid);
 }
index 8768b1d562556fa44e0cb37d175e1d27bcd30871..2e4eccf5f87b4f11afbcea031f4adcf20262ee29 100644 (file)
@@ -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) {
index 44ee3c5c6742cb0602db12846ad98993dbfa8184..92e29cb833c74f1d27c61f54c26376e24d2c8803 100644 (file)
@@ -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<String> userUuids = Set.of("a", "b");
-    DelegatingManagedServices managedInstanceService = NO_MANAGED_SERVICES;
 
-    Map<String, Boolean> userUuidToManaged = managedInstanceService.getUserUuidToManaged(dbSession, userUuids);
+    Map<String, Boolean> 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<ManagedInstanceService> 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 (file)
index 0000000..d31a865
--- /dev/null
@@ -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;
+  }
+}
index 140d53a9c914b3ee2f8b85dd4554442a5922964f..5552328191c29510b0b49c31a28dcdb48c96d002 100644 (file)
@@ -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
index f59b4cf84c617b970b07172fa60c8393a2f57460..41e7791358844850e20d0e5e912d26c9d306f2bb 100644 (file)
@@ -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);
     }
   }
index 737a5a5e0bac4d10ffe448d9d8a420dcd2ea5637..a19a7f5acf5df320669fc3614e8a9da1c75078cf 100644 (file)
@@ -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.