aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-common
diff options
context:
space:
mode:
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>2024-03-19 14:15:49 +0100
committersonartech <sonartech@sonarsource.com>2024-03-28 20:02:50 +0000
commit412f42cb112802614e541ca186d3e35006f28be9 (patch)
tree65afa0df4c797929ee01d79ec0ba6efb766b82fb /server/sonar-webserver-common
parent1398b6005bc206cd64bf570b73ee18092fe88a23 (diff)
downloadsonarqube-412f42cb112802614e541ca186d3e35006f28be9.tar.gz
sonarqube-412f42cb112802614e541ca186d3e35006f28be9.zip
SONAR-21819 Extract logic reusable between legacy and v2 endpoints.
Diffstat (limited to 'server/sonar-webserver-common')
-rw-r--r--server/sonar-webserver-common/build.gradle4
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java545
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/DefaultTemplatesResolverImplIT.java110
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/GroupPermissionChangerIT.java408
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/PermissionTemplateServiceIT.java484
-rw-r--r--server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/UserPermissionChangerIT.java346
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/ProjectKeyGenerator.java63
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactory.java54
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreator.java34
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreatorFactory.java33
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectDescriptor.java25
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreationParameters.java32
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreator.java229
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactory.java174
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreator.java145
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactory.java72
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentCreationParameters.java78
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java259
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/NewComponent.java120
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/CaycUtils.java39
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolver.java149
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolver.java68
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolverImpl.java71
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GranteeTypeSpecificPermissionUpdater.java32
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChange.java53
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChanger.java182
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionChange.java77
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionTemplateService.java243
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionUpdater.java78
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChange.java48
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChanger.java142
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ProjectCreator.java71
-rw-r--r--server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/package-info.java23
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almintegration/ProjectKeyGeneratorTest.java92
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactoryTest.java63
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactoryTest.java269
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorTest.java478
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactoryTest.java66
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java219
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/CaycUtilsTest.java85
-rw-r--r--server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolverTest.java147
47 files changed, 6048 insertions, 0 deletions
diff --git a/server/sonar-webserver-common/build.gradle b/server/sonar-webserver-common/build.gradle
index d12d353e29b..89ce2803657 100644
--- a/server/sonar-webserver-common/build.gradle
+++ b/server/sonar-webserver-common/build.gradle
@@ -26,11 +26,15 @@ dependencies {
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.mockito:mockito-core'
+ testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation project(':sonar-testing-harness')
testImplementation testFixtures(project(':server:sonar-db-dao'))
testImplementation testFixtures(project(':server:sonar-server-common'))
testImplementation testFixtures(project(':server:sonar-webserver-api'))
+ testImplementation testFixtures(project(':server:sonar-webserver-auth'))
+ testImplementation testFixtures(project(':server:sonar-webserver-es'))
+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java
new file mode 100644
index 00000000000..30a70a126ef
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/component/ComponentUpdaterIT.java
@@ -0,0 +1,545 @@
+/*
+ * 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.component;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.audit.AuditPersister;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.permission.GroupPermissionChanger;
+import org.sonar.server.common.permission.PermissionTemplateService;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.common.permission.UserPermissionChanger;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.Indexers;
+import org.sonar.server.es.IndexersImpl;
+import org.sonar.server.es.TestIndexers;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.favorite.FavoriteUpdater;
+import org.sonar.server.l18n.I18nRule;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.permission.PermissionServiceImpl;
+import org.sonar.server.permission.index.FooIndexDefinition;
+import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.project.DefaultBranchNameResolver;
+
+import static java.util.stream.IntStream.rangeClosed;
+import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.resources.Qualifiers.APP;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.resources.Qualifiers.VIEW;
+import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME;
+
+public class ComponentUpdaterIT {
+
+ private static final String DEFAULT_PROJECT_KEY = "project-key";
+ private static final String DEFAULT_PROJECT_NAME = "project-name";
+ private static final NewComponent DEFAULT_COMPONENT = NewComponent.newComponentBuilder()
+ .setKey(DEFAULT_PROJECT_KEY)
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ private static final NewComponent PRIVATE_COMPONENT = NewComponent.newComponentBuilder()
+ .setKey(DEFAULT_PROJECT_KEY)
+ .setName(DEFAULT_PROJECT_NAME)
+ .setPrivate(true)
+ .build();
+ private static final String DEFAULT_USER_UUID = "user-uuid";
+ public static final String DEFAULT_USER_LOGIN = "user-login";
+
+ private final System2 system2 = System2.INSTANCE;
+
+ private final AuditPersister auditPersister = mock();
+
+ @Rule
+ public final DbTester db = DbTester.create(system2, auditPersister);
+ @Rule
+ public final I18nRule i18n = new I18nRule().put("qualifier.TRK", "Project");
+
+ private final TestIndexers projectIndexers = new TestIndexers();
+ private final PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class);
+ private final DefaultBranchNameResolver defaultBranchNameResolver = mock(DefaultBranchNameResolver.class);
+ public EsTester es = EsTester.createCustom(new FooIndexDefinition());
+ private final PermissionUpdater<UserPermissionChange> userPermissionUpdater = new PermissionUpdater(
+ new IndexersImpl(new PermissionIndexer(db.getDbClient(), es.client())),
+ Set.of(new UserPermissionChanger(db.getDbClient(), new SequenceUuidFactory()),
+ new GroupPermissionChanger(db.getDbClient(), new SequenceUuidFactory())));
+ private final PermissionService permissionService = new PermissionServiceImpl(new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT));
+
+ private final ComponentUpdater underTest = new ComponentUpdater(db.getDbClient(), i18n, system2,
+ permissionTemplateService,
+ new FavoriteUpdater(db.getDbClient()),
+ projectIndexers, new SequenceUuidFactory(), defaultBranchNameResolver, userPermissionUpdater, permissionService);
+
+ @Before
+ public void before() {
+ when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn(DEFAULT_MAIN_BRANCH_NAME);
+ }
+
+ @Test
+ public void persist_and_index_when_creating_project() {
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(PRIVATE_COMPONENT)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentCreationData returned = underTest.create(db.getSession(), creationParameters);
+
+ ComponentDto loaded = db.getDbClient().componentDao().selectOrFailByUuid(db.getSession(), returned.mainBranchComponent().uuid());
+ assertThat(loaded.getKey()).isEqualTo(DEFAULT_PROJECT_KEY);
+ assertThat(loaded.name()).isEqualTo(DEFAULT_PROJECT_NAME);
+ assertThat(loaded.longName()).isEqualTo(DEFAULT_PROJECT_NAME);
+ assertThat(loaded.qualifier()).isEqualTo(Qualifiers.PROJECT);
+ assertThat(loaded.scope()).isEqualTo(Scopes.PROJECT);
+ assertThat(loaded.uuid()).isNotNull();
+ assertThat(loaded.branchUuid()).isEqualTo(loaded.uuid());
+ assertThat(loaded.isPrivate()).isEqualTo(PRIVATE_COMPONENT.isPrivate());
+ assertThat(loaded.getCreatedAt()).isNotNull();
+ assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY)).isPresent();
+
+ assertThat(projectIndexers.hasBeenCalledForEntity(returned.projectDto().getUuid(), Indexers.EntityEvent.CREATION)).isTrue();
+
+ Optional<BranchDto> branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), returned.mainBranchComponent().uuid());
+ assertThat(branch).isPresent();
+ assertThat(branch.get().getKey()).isEqualTo(DEFAULT_MAIN_BRANCH_NAME);
+ assertThat(branch.get().getMergeBranchUuid()).isNull();
+ assertThat(branch.get().getBranchType()).isEqualTo(BranchType.BRANCH);
+ assertThat(branch.get().getUuid()).isEqualTo(returned.mainBranchComponent().uuid());
+ assertThat(branch.get().getProjectUuid()).isEqualTo(returned.projectDto().getUuid());
+ }
+
+ @Test
+ public void create_project_with_main_branch_global_property() {
+ when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn("main-branch-global");
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(PRIVATE_COMPONENT)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ ComponentDto returned = underTest.create(db.getSession(), creationParameters).mainBranchComponent();
+
+ Optional<BranchDto> branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), returned.branchUuid());
+ assertThat(branch).get().extracting(BranchDto::getBranchKey).isEqualTo("main-branch-global");
+ }
+
+ @Test
+ public void persist_private_flag_true_when_creating_project() {
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(PRIVATE_COMPONENT)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentDto returned = underTest.create(db.getSession(), creationParameters).mainBranchComponent();
+ ComponentDto loaded = db.getDbClient().componentDao().selectOrFailByUuid(db.getSession(), returned.uuid());
+ assertThat(loaded.isPrivate()).isEqualTo(PRIVATE_COMPONENT.isPrivate());
+ }
+
+ @Test
+ public void persist_private_flag_false_when_creating_project() {
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey(DEFAULT_PROJECT_KEY)
+ .setName(DEFAULT_PROJECT_NAME)
+ .setPrivate(false)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentDto returned = underTest.create(db.getSession(), creationParameters).mainBranchComponent();
+ ComponentDto loaded = db.getDbClient().componentDao().selectOrFailByUuid(db.getSession(), returned.uuid());
+ assertThat(loaded.isPrivate()).isEqualTo(project.isPrivate());
+ }
+
+ @Test
+ public void create_view() {
+ NewComponent view = NewComponent.newComponentBuilder()
+ .setKey("view-key")
+ .setName("view-name")
+ .setQualifier(VIEW)
+ .build();
+
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(view)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentDto returned = underTest.create(db.getSession(), creationParameters).mainBranchComponent();
+
+ ComponentDto loaded = db.getDbClient().componentDao().selectOrFailByUuid(db.getSession(), returned.uuid());
+ assertThat(loaded.getKey()).isEqualTo("view-key");
+ assertThat(loaded.name()).isEqualTo("view-name");
+ assertThat(loaded.qualifier()).isEqualTo("VW");
+ assertThat(projectIndexers.hasBeenCalledForEntity(loaded.uuid(), Indexers.EntityEvent.CREATION)).isTrue();
+ Optional<BranchDto> branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), returned.uuid());
+ assertThat(branch).isNotPresent();
+ }
+
+ @Test
+ public void create_application() {
+ NewComponent application = NewComponent.newComponentBuilder()
+ .setKey("app-key")
+ .setName("app-name")
+ .setQualifier(APP)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(application)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentCreationData returned = underTest.create(db.getSession(), creationParameters);
+
+ ProjectDto loaded = db.getDbClient().projectDao().selectByUuid(db.getSession(), returned.projectDto().getUuid()).get();
+ assertThat(loaded.getKey()).isEqualTo("app-key");
+ assertThat(loaded.getName()).isEqualTo("app-name");
+ assertThat(loaded.getQualifier()).isEqualTo("APP");
+ assertThat(projectIndexers.hasBeenCalledForEntity(loaded.getUuid(), Indexers.EntityEvent.CREATION)).isTrue();
+ Optional<BranchDto> branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), returned.mainBranchComponent().uuid());
+ assertThat(branch).isPresent();
+ assertThat(branch.get().getKey()).isEqualTo(DEFAULT_MAIN_BRANCH_NAME);
+ assertThat(branch.get().getMergeBranchUuid()).isNull();
+ assertThat(branch.get().getBranchType()).isEqualTo(BranchType.BRANCH);
+ assertThat(branch.get().getUuid()).isEqualTo(returned.mainBranchComponent().uuid());
+ assertThat(branch.get().getProjectUuid()).isEqualTo(returned.projectDto().getUuid());
+ }
+
+ @Test
+ public void apply_default_permission_template() {
+ ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .userLogin(DEFAULT_USER_LOGIN)
+ .userUuid(DEFAULT_USER_UUID)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ ProjectDto dto = underTest.create(db.getSession(), componentCreationParameters).projectDto();
+
+ verify(permissionTemplateService).applyDefaultToNewComponent(db.getSession(), dto, DEFAULT_USER_UUID);
+ }
+
+ @Test
+ public void add_project_to_user_favorites_if_project_creator_is_defined_in_permission_template() {
+ UserDto userDto = db.users().insertUser();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .userLogin(userDto.getLogin())
+ .userUuid(userDto.getUuid())
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ when(permissionTemplateService.hasDefaultTemplateWithPermissionOnProjectCreator(any(DbSession.class), any(ProjectDto.class)))
+ .thenReturn(true);
+
+ ProjectDto dto = underTest.create(db.getSession(), creationParameters).projectDto();
+
+ assertThat(db.favorites().hasFavorite(dto, userDto.getUuid())).isTrue();
+ }
+
+ @Test
+ public void do_not_add_project_to_user_favorites_if_project_creator_is_defined_in_permission_template_and_already_100_favorites() {
+ UserDto user = db.users().insertUser();
+ rangeClosed(1, 100).forEach(i -> db.favorites().add(db.components().insertPrivateProject().getProjectDto(), user.getUuid(), user.getLogin()));
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .userLogin(user.getLogin())
+ .userUuid(user.getUuid())
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ when(permissionTemplateService.hasDefaultTemplateWithPermissionOnProjectCreator(eq(db.getSession()), any(ProjectDto.class)))
+ .thenReturn(true);
+
+ ProjectDto dto = underTest.create(db.getSession(), creationParameters).projectDto();
+
+ assertThat(db.favorites().hasFavorite(dto, user.getUuid())).isFalse();
+ }
+
+ @Test
+ public void does_not_add_project_to_favorite_when_anonymously_created() {
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ProjectDto projectDto = underTest.create(db.getSession(), creationParameters).projectDto();
+
+ assertThat(db.favorites().hasNoFavorite(projectDto)).isTrue();
+ }
+
+ @Test
+ public void fail_when_project_key_already_exists() {
+ ComponentDto existing = db.components().insertPrivateProject().getMainBranchComponent();
+ DbSession session = db.getSession();
+
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey(existing.getKey())
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ assertThatThrownBy(() -> underTest.create(session, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Could not create Project with key: \"%s\". A similar key already exists: \"%s\"", existing.getKey(), existing.getKey());
+ }
+
+ @Test
+ public void fail_when_key_has_bad_format() {
+ DbSession session = db.getSession();
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey("1234")
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ assertThatThrownBy(() -> underTest.create(session, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessageContaining("Malformed key for Project: '1234'");
+ }
+
+ @Test
+ public void fail_when_key_contains_percent_character() {
+ DbSession session = db.getSession();
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey("roject%Key")
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ assertThatThrownBy(() -> underTest.create(session, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessageContaining("Malformed key for Project: 'roject%Key'");
+ }
+
+ @Test
+ public void create_shouldFail_whenCreatingProjectWithExistingKeyButDifferentCase() {
+ createComponent_shouldFail_whenCreatingComponentWithExistingKeyButDifferentCase(PROJECT);
+ }
+
+ @Test
+ public void create_shouldFail_whenCreatingPortfolioWithExistingKeyButDifferentCase() {
+ createComponent_shouldFail_whenCreatingComponentWithExistingKeyButDifferentCase(VIEW);
+ }
+
+ @Test
+ public void create_shouldFail_whenCreatingApplicationWithExistingKeyButDifferentCase() {
+ createComponent_shouldFail_whenCreatingComponentWithExistingKeyButDifferentCase(APP);
+ }
+
+ private void createComponent_shouldFail_whenCreatingComponentWithExistingKeyButDifferentCase(String qualifier) {
+ String existingKey = randomAlphabetic(5).toUpperCase();
+ db.components().insertPrivateProject(component -> component.setKey(existingKey));
+ String newKey = existingKey.toLowerCase();
+
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey(newKey)
+ .setName(DEFAULT_PROJECT_NAME)
+ .setQualifier(qualifier)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ DbSession dbSession = db.getSession();
+ assertThatThrownBy(() -> underTest.create(dbSession, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Could not create Project with key: \"%s\". A similar key already exists: \"%s\"", newKey, existingKey);
+ }
+
+ @Test
+ public void createComponent_shouldFail_whenCreatingComponentWithMultipleExistingKeyButDifferentCase() {
+ String existingKey = randomAlphabetic(5).toUpperCase();
+ String existingKeyLowerCase = existingKey.toLowerCase();
+ db.components().insertPrivateProject(component -> component.setKey(existingKey));
+ db.components().insertPrivateProject(component -> component.setKey(existingKeyLowerCase));
+ String newKey = StringUtils.capitalize(existingKeyLowerCase);
+
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey(newKey)
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ DbSession dbSession = db.getSession();
+ assertThatThrownBy(() -> underTest.create(dbSession, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Could not create Project with key: \"%s\". A similar key already exists: \"%s, %s\"", newKey, existingKey, existingKeyLowerCase);
+ }
+
+ @Test
+ public void createComponent_shouldFail_whenCreatingComponentWithMultipleExistingPortfolioKeysButDifferentCase() {
+ String existingKey = randomAlphabetic(5).toUpperCase();
+ String existingKeyLowerCase = existingKey.toLowerCase();
+ db.components().insertPrivatePortfolio(portfolio -> portfolio.setKey(existingKey));
+ db.components().insertPrivatePortfolio(portfolio -> portfolio.setKey(existingKeyLowerCase));
+ String newKey = StringUtils.capitalize(existingKeyLowerCase);
+
+ NewComponent project = NewComponent.newComponentBuilder()
+ .setKey(newKey)
+ .setName(DEFAULT_PROJECT_NAME)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(project)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ DbSession dbSession = db.getSession();
+ assertThatThrownBy(() -> underTest.create(dbSession, creationParameters))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Could not create Project with key: \"%s\". A similar key already exists: \"%s, %s\"", newKey, existingKey, existingKeyLowerCase);
+ }
+
+ @Test
+ public void create_createsComponentWithMasterBranchName() {
+ String componentNameAndKey = "createApplicationOrPortfolio";
+ NewComponent app = NewComponent.newComponentBuilder()
+ .setKey(componentNameAndKey)
+ .setName(componentNameAndKey)
+ .setQualifier("APP")
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(app)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ ComponentDto appDto = underTest.create(db.getSession(), creationParameters).mainBranchComponent();
+
+ Optional<BranchDto> branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), appDto.branchUuid());
+ assertThat(branch).isPresent();
+ assertThat(branch.get().getBranchKey()).isEqualTo(DEFAULT_MAIN_BRANCH_NAME);
+ }
+
+ @Test
+ public void createWithoutCommit_whenProjectIsManaged_doesntApplyPermissionTemplate() {
+ UserDto userDto = db.users().insertUser();
+ ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .userLogin(userDto.getLogin())
+ .userUuid(userDto.getUuid())
+ .mainBranchName(null)
+ .isManaged(true)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ underTest.createWithoutCommit(db.getSession(), componentCreationParameters);
+
+ verify(permissionTemplateService, never()).applyDefaultToNewComponent(any(), any(), any());
+ }
+
+ @Test
+ public void createWithoutCommit_whenInsertingPortfolio_shouldOnlyAddOneEntryToAuditLogs() {
+ String portfolioKey = "portfolio";
+ NewComponent portfolio = NewComponent.newComponentBuilder()
+ .setKey(portfolioKey)
+ .setName(portfolioKey)
+ .setQualifier(VIEW)
+ .build();
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(portfolio)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+
+ underTest.createWithoutCommit(db.getSession(), creationParameters);
+ db.commit();
+
+ verify(auditPersister, times(1)).addComponent(argThat(d -> d.equals(db.getSession())),
+ argThat(newValue -> newValue.getComponentKey().equals(portfolioKey)));
+ }
+
+ @Test
+ public void createWithoutCommit_whenProjectIsManagedAndPrivate_applyPublicPermissionsToCreator() {
+ UserDto userDto = db.users().insertUser();
+ NewComponent newComponent = NewComponent.newComponentBuilder()
+ .setKey(DEFAULT_PROJECT_KEY)
+ .setName(DEFAULT_PROJECT_NAME)
+ .setPrivate(true)
+ .build();
+
+ DbSession session = db.getSession();
+
+ ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
+ .newComponent(PRIVATE_COMPONENT)
+ .userLogin(userDto.getLogin())
+ .userUuid(userDto.getUuid())
+ .mainBranchName(null)
+ .isManaged(true)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ComponentCreationData componentCreationData = underTest.createWithoutCommit(session, componentCreationParameters);
+
+ List<String> permissions = db.getDbClient().userPermissionDao().selectEntityPermissionsOfUser(session, userDto.getUuid(), componentCreationData.projectDto().getUuid());
+ assertThat(permissions)
+ .containsExactlyInAnyOrder(UserRole.USER, UserRole.CODEVIEWER);
+ }
+
+ @Test
+ public void create_whenCreationMethodIsLocalApi_persistsIt() {
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .creationMethod(CreationMethod.LOCAL_API)
+ .build();
+ ProjectDto projectDto = underTest.create(db.getSession(), creationParameters).projectDto();
+ assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.LOCAL_API);
+ }
+
+ @Test
+ public void create_whenCreationMethodIsAlmImportBrowser_persistsIt() {
+ ComponentCreationParameters creationParameters = ComponentCreationParameters.builder()
+ .newComponent(DEFAULT_COMPONENT)
+ .creationMethod(CreationMethod.ALM_IMPORT_BROWSER)
+ .build();
+ ProjectDto projectDto = underTest.create(db.getSession(), creationParameters).projectDto();
+ assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_BROWSER);
+ }
+}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/DefaultTemplatesResolverImplIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/DefaultTemplatesResolverImplIT.java
new file mode 100644
index 00000000000..2a3716ab1af
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/DefaultTemplatesResolverImplIT.java
@@ -0,0 +1,110 @@
+/*
+ * 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.permission;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ResourceTypesRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.api.resources.Qualifiers.APP;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.resources.Qualifiers.VIEW;
+
+public class DefaultTemplatesResolverImplIT {
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private ResourceTypesRule resourceTypesWithPortfoliosInstalled = new ResourceTypesRule().setRootQualifiers(PROJECT, APP, VIEW);
+ private ResourceTypesRule resourceTypesWithApplicationInstalled = new ResourceTypesRule().setRootQualifiers(PROJECT, APP);
+ private ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT);
+
+ private DefaultTemplatesResolverImpl underTestWithPortfoliosInstalled = new DefaultTemplatesResolverImpl(db.getDbClient(), resourceTypesWithPortfoliosInstalled);
+ private DefaultTemplatesResolverImpl underTestWithApplicationInstalled = new DefaultTemplatesResolverImpl(db.getDbClient(), resourceTypesWithApplicationInstalled);
+ private DefaultTemplatesResolverImpl underTest = new DefaultTemplatesResolverImpl(db.getDbClient(), resourceTypes);
+
+ @Test
+ public void get_default_templates_when_portfolio_not_installed() {
+ db.permissionTemplates().setDefaultTemplates("prj", null, null);
+
+ assertThat(underTest.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTest.resolve(db.getSession()).getApplication()).isEmpty();
+ assertThat(underTest.resolve(db.getSession()).getPortfolio()).isEmpty();
+ }
+
+ @Test
+ public void get_default_templates_always_return_project_template_even_when_all_templates_are_defined_but_portfolio_not_installed() {
+ db.permissionTemplates().setDefaultTemplates("prj", "app", "port");
+
+ assertThat(underTest.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTest.resolve(db.getSession()).getApplication()).isEmpty();
+ assertThat(underTest.resolve(db.getSession()).getPortfolio()).isEmpty();
+ }
+
+ @Test
+ public void get_default_templates_always_return_project_template_when_only_project_template_and_portfolio_is_installed_() {
+ db.permissionTemplates().setDefaultTemplates("prj", null, null);
+
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getApplication()).contains("prj");
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getPortfolio()).contains("prj");
+ }
+
+ @Test
+ public void get_default_templates_for_all_components_when_portfolio_is_installed() {
+ db.permissionTemplates().setDefaultTemplates("prj", "app", "port");
+
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getApplication()).contains("app");
+ assertThat(underTestWithPortfoliosInstalled.resolve(db.getSession()).getPortfolio()).contains("port");
+ }
+
+ @Test
+ public void get_default_templates_always_return_project_template_when_only_project_template_and_application_is_installed_() {
+ db.permissionTemplates().setDefaultTemplates("prj", null, null);
+
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getApplication()).contains("prj");
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getPortfolio()).isEmpty();
+ }
+
+ @Test
+ public void get_default_templates_for_all_components_when_application_is_installed() {
+ db.permissionTemplates().setDefaultTemplates("prj", "app", null);
+
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getProject()).contains("prj");
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getApplication()).contains("app");
+ assertThat(underTestWithApplicationInstalled.resolve(db.getSession()).getPortfolio()).isEmpty();
+ }
+
+ @Test
+ public void fail_when_default_template_for_project_is_missing() {
+ DbSession session = db.getSession();
+ assertThatThrownBy(() -> underTestWithPortfoliosInstalled.resolve(session))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Default template for project is missing");
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/GroupPermissionChangerIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/GroupPermissionChangerIT.java
new file mode 100644
index 00000000000..03e760bea6b
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/GroupPermissionChangerIT.java
@@ -0,0 +1,408 @@
+/*
+ * 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.permission;
+
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.permission.GroupPermissionChange;
+import org.sonar.server.common.permission.GroupPermissionChanger;
+import org.sonar.server.common.permission.Operation;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.permission.PermissionServiceImpl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES;
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+
+public class GroupPermissionChangerIT {
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private final ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
+ private final PermissionService permissionService = new PermissionServiceImpl(resourceTypes);
+ private final GroupPermissionChanger underTest = new GroupPermissionChanger(db.getDbClient(), new SequenceUuidFactory());
+ private GroupDto group;
+
+ private ProjectDto privateProject;
+ private ProjectDto publicProject;
+
+ @Before
+ public void setUp() {
+ group = db.users().insertGroup("a-group");
+ privateProject = db.components().insertPrivateProject().getProjectDto();
+ publicProject = db.components().insertPublicProject().getProjectDto();
+ }
+
+ @Test
+ public void apply_adds_global_permission_to_group() {
+ apply(new GroupPermissionChange(Operation.ADD, ADMINISTER_QUALITY_PROFILES.getKey(), null, group, permissionService));
+
+ assertThat(db.users().selectGroupPermissions(group, null)).containsOnly(ADMINISTER_QUALITY_PROFILES.getKey());
+ }
+
+ @Test
+ public void apply_adds_global_permission_to_group_AnyOne() {
+ apply(new GroupPermissionChange(Operation.ADD, ADMINISTER_QUALITY_PROFILES.getKey(), null, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(null)).containsOnly(ADMINISTER_QUALITY_PROFILES.getKey());
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_adding_any_permission_to_group_AnyOne_on_private_project() {
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.ADD, perm, privateProject, null, permissionService);
+ try {
+ apply(change);
+ fail("a BadRequestException should have been thrown");
+ } catch (BadRequestException e) {
+ assertThat(e).hasMessage("No permission can be granted to Anyone on a private component");
+ }
+ });
+ }
+
+ @Test
+ public void apply_has_no_effect_when_removing_any_permission_to_group_AnyOne_on_private_project() {
+ permissionService.getAllProjectPermissions()
+ .forEach(this::unsafeInsertProjectPermissionOnAnyone);
+
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> {
+ apply(new GroupPermissionChange(Operation.REMOVE, perm, privateProject, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(privateProject.getUuid())).contains(perm);
+ });
+ }
+
+ @Test
+ public void apply_adds_permission_USER_to_group_on_private_project() {
+ applyAddsPermissionToGroupOnPrivateProject(UserRole.USER);
+ }
+
+ @Test
+ public void apply_adds_permission_CODEVIEWER_to_group_on_private_project() {
+ applyAddsPermissionToGroupOnPrivateProject(UserRole.CODEVIEWER);
+ }
+
+ @Test
+ public void apply_adds_permission_ADMIN_to_group_on_private_project() {
+ applyAddsPermissionToGroupOnPrivateProject(UserRole.ADMIN);
+ }
+
+ @Test
+ public void apply_adds_permission_ISSUE_ADMIN_to_group_on_private_project() {
+ applyAddsPermissionToGroupOnPrivateProject(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_adds_permission_SCAN_EXECUTION_to_group_on_private_project() {
+ applyAddsPermissionToGroupOnPrivateProject(GlobalPermission.SCAN.getKey());
+ }
+
+ private void applyAddsPermissionToGroupOnPrivateProject(String permission) {
+
+ apply(new GroupPermissionChange(Operation.ADD, permission, privateProject, group, permissionService));
+
+ assertThat(db.users().selectGroupPermissions(group, null)).isEmpty();
+ assertThat(db.users().selectGroupPermissions(group, privateProject)).containsOnly(permission);
+ }
+
+ @Test
+ public void apply_removes_permission_USER_from_group_on_private_project() {
+ applyRemovesPermissionFromGroupOnPrivateProject(UserRole.USER);
+ }
+
+ @Test
+ public void apply_removes_permission_CODEVIEWER_from_group_on_private_project() {
+ applyRemovesPermissionFromGroupOnPrivateProject(UserRole.CODEVIEWER);
+ }
+
+ @Test
+ public void apply_removes_permission_ADMIN_from_on_private_project() {
+ applyRemovesPermissionFromGroupOnPrivateProject(UserRole.ADMIN);
+ }
+
+ @Test
+ public void apply_removes_permission_ISSUE_ADMIN_from_on_private_project() {
+ applyRemovesPermissionFromGroupOnPrivateProject(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_removes_permission_SCAN_EXECUTION_from_on_private_project() {
+ applyRemovesPermissionFromGroupOnPrivateProject(GlobalPermission.SCAN.getKey());
+ }
+
+ private void applyRemovesPermissionFromGroupOnPrivateProject(String permission) {
+ db.users().insertEntityPermissionOnGroup(group, permission, privateProject);
+
+ apply(new GroupPermissionChange(Operation.ADD, permission, privateProject, group, permissionService), permission);
+
+ assertThat(db.users().selectGroupPermissions(group, privateProject)).containsOnly(permission);
+ }
+
+ @Test
+ public void apply_has_no_effect_when_adding_USER_permission_to_group_AnyOne_on_a_public_project() {
+ apply(new GroupPermissionChange(Operation.ADD, UserRole.USER, publicProject, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(publicProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_has_no_effect_when_adding_CODEVIEWER_permission_to_group_AnyOne_on_a_public_project() {
+ apply(new GroupPermissionChange(Operation.ADD, UserRole.CODEVIEWER, publicProject, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(publicProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_adding_permission_ADMIN_to_group_AnyOne_on_a_public_project() {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.ADD, UserRole.ADMIN, publicProject, null, permissionService);
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("It is not possible to add the 'admin' permission to group 'Anyone'.");
+ }
+
+ @Test
+ public void apply_adds_permission_ISSUE_ADMIN_to_group_AnyOne_on_a_public_project() {
+ apply(new GroupPermissionChange(Operation.ADD, UserRole.ISSUE_ADMIN, publicProject, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(publicProject.getUuid())).containsOnly(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_adds_permission_SCAN_EXECUTION_to_group_AnyOne_on_a_public_project() {
+ apply(new GroupPermissionChange(Operation.ADD, GlobalPermission.SCAN.getKey(), publicProject, null, permissionService));
+
+ assertThat(db.users().selectAnyonePermissions(publicProject.getUuid())).containsOnly(GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_USER_permission_from_group_AnyOne_on_a_public_project() {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.REMOVE, UserRole.USER, publicProject, null, permissionService);
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission user can't be removed from a public component");
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_CODEVIEWER_permission_from_group_AnyOne_on_a_public_project() {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.REMOVE, UserRole.CODEVIEWER, publicProject, null, permissionService);
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission codeviewer can't be removed from a public component");
+ }
+
+ @Test
+ public void apply_removes_ADMIN_permission_from_group_AnyOne_on_a_public_project() {
+ applyRemovesPermissionFromGroupAnyOneOnAPublicProject(UserRole.ADMIN);
+ }
+
+ @Test
+ public void apply_removes_ISSUE_ADMIN_permission_from_group_AnyOne_on_a_public_project() {
+ applyRemovesPermissionFromGroupAnyOneOnAPublicProject(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_removes_SCAN_EXECUTION_permission_from_group_AnyOne_on_a_public_project() {
+ applyRemovesPermissionFromGroupAnyOneOnAPublicProject(GlobalPermission.SCAN.getKey());
+ }
+
+ private void applyRemovesPermissionFromGroupAnyOneOnAPublicProject(String permission) {
+ db.users().insertEntityPermissionOnAnyone(permission, publicProject);
+
+ apply(new GroupPermissionChange(Operation.REMOVE, permission, publicProject, null, permissionService), permission);
+
+ assertThat(db.users().selectAnyonePermissions(publicProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_USER_permission_from_a_group_on_a_public_project() {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.REMOVE, UserRole.USER, publicProject, group, permissionService);
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission user can't be removed from a public component");
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_CODEVIEWER_permission_from_a_group_on_a_public_project() {
+ GroupPermissionChange change = new GroupPermissionChange(Operation.REMOVE, UserRole.CODEVIEWER, publicProject, group, permissionService);
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission codeviewer can't be removed from a public component");
+ }
+
+ @Test
+ public void add_permission_to_anyone() {
+ apply(new GroupPermissionChange(Operation.ADD, ADMINISTER_QUALITY_PROFILES.getKey(), null, null, permissionService));
+
+ assertThat(db.users().selectGroupPermissions(group, null)).isEmpty();
+ assertThat(db.users().selectAnyonePermissions(null)).containsOnly(ADMINISTER_QUALITY_PROFILES.getKey());
+ }
+
+ @Test
+ public void do_nothing_when_adding_permission_that_already_exists() {
+ db.users().insertPermissionOnGroup(group, ADMINISTER_QUALITY_GATES);
+
+ apply(new GroupPermissionChange(Operation.ADD, ADMINISTER_QUALITY_GATES.getKey(), null, group, permissionService), ADMINISTER_QUALITY_GATES.getKey());
+
+ assertThat(db.users().selectGroupPermissions(group, null)).containsExactly(ADMINISTER_QUALITY_GATES.getKey());
+ }
+
+ @Test
+ public void fail_to_add_global_permission_but_SCAN_and_ADMIN_on_private_project() {
+ permissionService.getGlobalPermissions().stream()
+ .map(GlobalPermission::getKey)
+ .filter(perm -> !UserRole.ADMIN.equals(perm) && !GlobalPermission.SCAN.getKey().equals(perm))
+ .forEach(perm -> {
+ try {
+ new GroupPermissionChange(Operation.ADD, perm, privateProject, group, permissionService);
+ fail("a BadRequestException should have been thrown for permission " + perm);
+ } catch (BadRequestException e) {
+ assertThat(e).hasMessage("Invalid project permission '" + perm +
+ "'. Valid values are [" + StringUtils.join(permissionService.getAllProjectPermissions(), ", ") + "]");
+ }
+ });
+ }
+
+ @Test
+ public void fail_to_add_global_permission_but_SCAN_and_ADMIN_on_public_project() {
+ permissionService.getGlobalPermissions().stream()
+ .map(GlobalPermission::getKey)
+ .filter(perm -> !UserRole.ADMIN.equals(perm) && !GlobalPermission.SCAN.getKey().equals(perm))
+ .forEach(perm -> {
+ try {
+ new GroupPermissionChange(Operation.ADD, perm, publicProject, group, permissionService);
+ fail("a BadRequestException should have been thrown for permission " + perm);
+ } catch (BadRequestException e) {
+ assertThat(e).hasMessage("Invalid project permission '" + perm +
+ "'. Valid values are [" + StringUtils.join(permissionService.getAllProjectPermissions(), ", ") + "]");
+ }
+ });
+ }
+
+ @Test
+ public void fail_to_add_project_permission_but_SCAN_and_ADMIN_on_global_group() {
+ permissionService.getAllProjectPermissions()
+ .stream()
+ .filter(perm -> !GlobalPermission.SCAN.getKey().equals(perm) && !GlobalPermission.ADMINISTER.getKey().equals(perm))
+ .forEach(permission -> {
+ try {
+ new GroupPermissionChange(Operation.ADD, permission, null, group, permissionService);
+ fail("a BadRequestException should have been thrown for permission " + permission);
+ } catch (BadRequestException e) {
+ assertThat(e).hasMessage("Invalid global permission '" + permission + "'. Valid values are [admin, gateadmin, profileadmin, provisioning, scan]");
+ }
+ });
+ }
+
+ @Test
+ public void remove_permission_from_group() {
+ db.users().insertPermissionOnGroup(group, ADMINISTER_QUALITY_GATES);
+ db.users().insertPermissionOnGroup(group, PROVISION_PROJECTS);
+
+ apply(new GroupPermissionChange(Operation.REMOVE, ADMINISTER_QUALITY_GATES.getKey(), null, group, permissionService), ADMINISTER_QUALITY_GATES.getKey(),
+ PROVISION_PROJECTS.getKey());
+
+ assertThat(db.users().selectGroupPermissions(group, null)).containsOnly(PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void remove_project_permission_from_group() {
+ db.users().insertPermissionOnGroup(group, ADMINISTER_QUALITY_GATES);
+ db.users().insertEntityPermissionOnGroup(group, UserRole.ISSUE_ADMIN, privateProject);
+ db.users().insertEntityPermissionOnGroup(group, UserRole.CODEVIEWER, privateProject);
+
+ apply(new GroupPermissionChange(Operation.REMOVE, UserRole.ISSUE_ADMIN, privateProject, group, permissionService), UserRole.ISSUE_ADMIN,
+ UserRole.CODEVIEWER);
+
+ assertThat(db.users().selectGroupPermissions(group, null)).containsOnly(ADMINISTER_QUALITY_GATES.getKey());
+ assertThat(db.users().selectGroupPermissions(group, privateProject)).containsOnly(UserRole.CODEVIEWER);
+ }
+
+ @Test
+ public void do_not_fail_if_removing_a_permission_that_does_not_exist() {
+ apply(new GroupPermissionChange(Operation.REMOVE, UserRole.ISSUE_ADMIN, privateProject, group, permissionService));
+
+ assertThat(db.users().selectGroupPermissions(group, null)).isEmpty();
+ assertThat(db.users().selectGroupPermissions(group, privateProject)).isEmpty();
+ }
+
+ @Test
+ public void fail_to_remove_admin_permission_if_no_more_admins() {
+ db.users().insertPermissionOnGroup(group, ADMINISTER);
+
+ GroupPermissionChange change = new GroupPermissionChange(Operation.REMOVE, ADMINISTER.getKey(), null, group, permissionService);
+ Set<String> permission = Set.of("admin");
+ DbSession session = db.getSession();
+ assertThatThrownBy(() -> underTest.apply(session, permission, change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Last group with permission 'admin'. Permission cannot be removed.");
+ }
+
+ @Test
+ public void remove_admin_group_if_still_other_admins() {
+ db.users().insertPermissionOnGroup(group, ADMINISTER);
+ UserDto admin = db.users().insertUser();
+ db.users().insertGlobalPermissionOnUser(admin, ADMINISTER);
+
+ apply(new GroupPermissionChange(Operation.REMOVE, ADMINISTER.getKey(), null, group, permissionService), ADMINISTER.getKey());
+
+ assertThat(db.users().selectGroupPermissions(group, null)).isEmpty();
+ }
+
+ private void apply(GroupPermissionChange change, String... existingPermissions) {
+ underTest.apply(db.getSession(), Set.of(existingPermissions), change);
+ db.commit();
+ }
+
+ private void unsafeInsertProjectPermissionOnAnyone(String perm) {
+ GroupPermissionDto dto = new GroupPermissionDto()
+ .setUuid(Uuids.createFast())
+ .setGroupUuid(null)
+ .setRole(perm)
+ .setEntityUuid(privateProject.getUuid())
+ .setEntityName(privateProject.getName());
+ db.getDbClient().groupPermissionDao().insert(db.getSession(), dto, privateProject, null);
+ db.commit();
+ }
+}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/PermissionTemplateServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/PermissionTemplateServiceIT.java
new file mode 100644
index 00000000000..d7095c13bcd
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/PermissionTemplateServiceIT.java
@@ -0,0 +1,484 @@
+/*
+ * 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.permission;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.template.PermissionTemplateDbTester;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.portfolio.PortfolioDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.es.Indexers;
+import org.sonar.server.es.TestIndexers;
+import org.sonar.server.exceptions.TemplateMatchingKeyException;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.permission.PermissionServiceImpl;
+import org.sonar.server.tester.UserSessionRule;
+
+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.sonar.api.resources.Qualifiers.APP;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.resources.Qualifiers.VIEW;
+
+public class PermissionTemplateServiceIT {
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ private final ResourceTypesRule resourceTypesRule = new ResourceTypesRule().setRootQualifiers(PROJECT, VIEW, APP);
+ private final DefaultTemplatesResolver defaultTemplatesResolver = new DefaultTemplatesResolverImpl(dbTester.getDbClient(), resourceTypesRule);
+ private final PermissionService permissionService = new PermissionServiceImpl(resourceTypesRule);
+ private final UserSessionRule userSession = UserSessionRule.standalone();
+ private final PermissionTemplateDbTester templateDb = dbTester.permissionTemplates();
+ private final DbSession session = dbTester.getSession();
+ private final Indexers indexers = new TestIndexers();
+ private final PermissionTemplateService underTest = new PermissionTemplateService(dbTester.getDbClient(), indexers, userSession, defaultTemplatesResolver,
+ new SequenceUuidFactory());
+
+ @Test
+ public void apply_does_not_insert_permission_to_group_AnyOne_when_applying_template_on_private_project() {
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
+
+ assertThat(selectProjectPermissionsOfGroup(null, privateProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_default_does_not_insert_permission_to_group_AnyOne_when_applying_template_on_private_project() {
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ UserDto creator = dbTester.users().insertUser();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, privateProject, creator.getUuid());
+
+ assertThat(selectProjectPermissionsOfGroup(null, privateProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_inserts_permissions_to_group_AnyOne_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, perm));
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
+
+ assertThat(selectProjectPermissionsOfGroup(null, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_permissions_to_group_AnyOne_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, perm));
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, publicProject, null);
+
+ assertThat(selectProjectPermissionsOfGroup(null, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_inserts_any_permissions_to_group_when_applying_template_on_private_project() {
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ GroupDto group = dbTester.users().insertGroup();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
+
+ assertThat(selectProjectPermissionsOfGroup(group, privateProject.getUuid()))
+ .containsOnly("p1", UserRole.CODEVIEWER, UserRole.USER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_any_permissions_to_group_when_applying_template_on_private_project() {
+ GroupDto group = dbTester.users().insertGroup();
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, privateProject, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, privateProject.getUuid()))
+ .containsOnly("p1", UserRole.CODEVIEWER, UserRole.USER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_inserts_permissions_to_group_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ GroupDto group = dbTester.users().insertGroup();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
+
+ assertThat(selectProjectPermissionsOfGroup(group, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_permissions_to_group_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ GroupDto group = dbTester.users().insertGroup();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, publicProject, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_inserts_permissions_to_user_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
+ dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
+
+ assertThat(selectProjectPermissionsOfUser(user, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_permissions_to_user_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
+ dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, publicProject, null);
+
+ assertThat(selectProjectPermissionsOfUser(user, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_inserts_any_permissions_to_user_when_applying_template_on_private_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
+ dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
+
+ assertThat(selectProjectPermissionsOfUser(user, privateProject.getUuid()))
+ .containsOnly("p1", UserRole.CODEVIEWER, UserRole.USER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_any_permissions_to_user_when_applying_template_on_private_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
+ dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, privateProject, null);
+
+ assertThat(selectProjectPermissionsOfUser(user, privateProject.getUuid()))
+ .containsOnly("p1", UserRole.CODEVIEWER, UserRole.USER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_permissions_to_ProjectCreator_but_USER_and_CODEVIEWER_when_applying_template_on_public_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto publicProject = dbTester.components().insertPublicProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addProjectCreatorToTemplate(permissionTemplate, perm));
+ dbTester.permissionTemplates().addProjectCreatorToTemplate(permissionTemplate, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, publicProject, user.getUuid());
+
+ assertThat(selectProjectPermissionsOfUser(user, publicProject.getUuid()))
+ .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void applyDefault_inserts_any_permissions_to_ProjectCreator_when_applying_template_on_private_project() {
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ ProjectDto privateProject = dbTester.components().insertPrivateProject().getProjectDto();
+ UserDto user = dbTester.users().insertUser();
+ permissionService.getAllProjectPermissions()
+ .forEach(perm -> dbTester.permissionTemplates().addProjectCreatorToTemplate(permissionTemplate, perm));
+ dbTester.permissionTemplates().addProjectCreatorToTemplate(permissionTemplate, "p1");
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, privateProject, user.getUuid());
+
+ assertThat(selectProjectPermissionsOfUser(user, privateProject.getUuid()))
+ .containsOnly("p1", UserRole.CODEVIEWER, UserRole.USER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, UserRole.SECURITYHOTSPOT_ADMIN, GlobalPermission.SCAN.getKey());
+ }
+
+ @Test
+ public void apply_template_on_view() {
+ PortfolioDto portfolio = dbTester.components().insertPrivatePortfolioDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, portfolio, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, portfolio.getUuid()))
+ .containsOnly(GlobalPermission.ADMINISTER.getKey(), GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_default_template_on_application() {
+ ProjectDto application = dbTester.components().insertPublicApplication().getProjectDto();
+ PermissionTemplateDto projectPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ PermissionTemplateDto appPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(appPermissionTemplate, group, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(appPermissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(projectPermissionTemplate, appPermissionTemplate, null);
+
+ underTest.applyDefaultToNewComponent(session, application, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, application.getUuid()))
+ .containsOnly(GlobalPermission.ADMINISTER.getKey(), GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_default_template_on_portfolio() {
+ PortfolioDto portfolio = dbTester.components().insertPublicPortfolioDto();
+ PermissionTemplateDto projectPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ PermissionTemplateDto portPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(portPermissionTemplate, group, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(portPermissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(projectPermissionTemplate, null, portPermissionTemplate);
+
+ underTest.applyDefaultToNewComponent(session, portfolio, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, portfolio.getUuid()))
+ .containsOnly(GlobalPermission.ADMINISTER.getKey(), GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_project_default_template_on_view_when_no_view_default_template() {
+ PortfolioDto portfolio = dbTester.components().insertPrivatePortfolioDto();
+ PermissionTemplateDto projectPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(projectPermissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(projectPermissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, portfolio, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, portfolio.getUuid())).containsOnly(GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_template_on_applications() {
+ ProjectDto application = dbTester.components().insertPublicApplication().getProjectDto();
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(permissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, application, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, application.getUuid()))
+ .containsOnly(GlobalPermission.ADMINISTER.getKey(), GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_default_view_template_on_application() {
+ ProjectDto application = dbTester.components().insertPublicApplication().getProjectDto();
+ PermissionTemplateDto projectPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ PermissionTemplateDto appPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ PermissionTemplateDto portPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(appPermissionTemplate, group, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(appPermissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(projectPermissionTemplate, appPermissionTemplate, portPermissionTemplate);
+
+ underTest.applyDefaultToNewComponent(session, application, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, application.getUuid()))
+ .containsOnly(GlobalPermission.ADMINISTER.getKey(), GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_project_default_template_on_application_when_no_application_default_template() {
+ ProjectDto application = dbTester.components().insertPublicApplication().getProjectDto();
+ PermissionTemplateDto projectPermissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ GroupDto group = dbTester.users().insertGroup();
+ dbTester.permissionTemplates().addGroupToTemplate(projectPermissionTemplate, group, GlobalPermission.PROVISION_PROJECTS.getKey());
+ dbTester.permissionTemplates().setDefaultTemplates(projectPermissionTemplate, null, null);
+
+ underTest.applyDefaultToNewComponent(session, application, null);
+
+ assertThat(selectProjectPermissionsOfGroup(group, application.getUuid())).containsOnly(GlobalPermission.PROVISION_PROJECTS.getKey());
+ }
+
+ @Test
+ public void apply_permission_template() {
+ UserDto user = dbTester.users().insertUser();
+ ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
+ GroupDto adminGroup = dbTester.users().insertGroup();
+ GroupDto userGroup = dbTester.users().insertGroup();
+ dbTester.users().insertPermissionOnGroup(adminGroup, GlobalPermission.ADMINISTER.getKey());
+ dbTester.users().insertPermissionOnGroup(userGroup, UserRole.USER);
+ dbTester.users().insertGlobalPermissionOnUser(user, GlobalPermission.ADMINISTER);
+ PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate();
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, adminGroup, GlobalPermission.ADMINISTER.getKey());
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, adminGroup, UserRole.ISSUE_ADMIN);
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, userGroup, UserRole.USER);
+ dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, userGroup, UserRole.CODEVIEWER);
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, UserRole.USER);
+ dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, UserRole.CODEVIEWER);
+ dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, GlobalPermission.ADMINISTER.getKey());
+
+ assertThat(selectProjectPermissionsOfGroup(adminGroup, project.getUuid())).isEmpty();
+ assertThat(selectProjectPermissionsOfGroup(userGroup, project.getUuid())).isEmpty();
+ assertThat(selectProjectPermissionsOfGroup(null, project.getUuid())).isEmpty();
+ assertThat(selectProjectPermissionsOfUser(user, project.getUuid())).isEmpty();
+
+ underTest.applyAndCommit(session, permissionTemplate, singletonList(project));
+
+ assertThat(selectProjectPermissionsOfGroup(adminGroup, project.getUuid())).containsOnly(GlobalPermission.ADMINISTER.getKey(), UserRole.ISSUE_ADMIN);
+ assertThat(selectProjectPermissionsOfGroup(userGroup, project.getUuid())).containsOnly(UserRole.USER, UserRole.CODEVIEWER);
+ assertThat(selectProjectPermissionsOfGroup(null, project.getUuid())).isEmpty();
+ assertThat(selectProjectPermissionsOfUser(user, project.getUuid())).containsOnly(GlobalPermission.ADMINISTER.getKey());
+ }
+
+ private List<String> selectProjectPermissionsOfGroup(@Nullable GroupDto groupDto, String projectUuid) {
+ return dbTester.getDbClient().groupPermissionDao().selectEntityPermissionsOfGroup(session, groupDto != null ? groupDto.getUuid() : null, projectUuid);
+ }
+
+ private List<String> selectProjectPermissionsOfUser(UserDto userDto, String projectUuid) {
+ return dbTester.getDbClient().userPermissionDao().selectEntityPermissionsOfUser(session, userDto.getUuid(), projectUuid);
+ }
+
+ @Test
+ public void would_user_have_scan_permission_with_default_permission_template() {
+ GroupDto group = dbTester.users().insertGroup();
+ UserDto user = dbTester.users().insertUser();
+ dbTester.users().insertMember(group, user);
+ PermissionTemplateDto template = templateDb.insertTemplate();
+ dbTester.permissionTemplates().setDefaultTemplates(template, null, null);
+ templateDb.addProjectCreatorToTemplate(template.getUuid(), GlobalPermission.SCAN.getKey(), template.getName());
+ templateDb.addUserToTemplate(template.getUuid(), user.getUuid(), UserRole.USER, template.getName(), user.getLogin());
+ templateDb.addGroupToTemplate(template.getUuid(), group.getUuid(), UserRole.CODEVIEWER, template.getName(), group.getName());
+ templateDb.addGroupToTemplate(template.getUuid(), null, UserRole.ISSUE_ADMIN, template.getName(), null);
+
+ // authenticated user
+ checkWouldUserHaveScanPermission(user.getUuid(), true);
+
+ // anonymous user
+ checkWouldUserHaveScanPermission(null, false);
+ }
+
+ @Test
+ public void would_user_have_scan_permission_with_unknown_default_permission_template() {
+ dbTester.permissionTemplates().setDefaultTemplates("UNKNOWN_TEMPLATE_UUID", null, null);
+
+ checkWouldUserHaveScanPermission(null, false);
+ }
+
+ @Test
+ public void would_user_have_scan_permission_with_empty_template() {
+ PermissionTemplateDto template = templateDb.insertTemplate();
+ dbTester.permissionTemplates().setDefaultTemplates(template, null, null);
+
+ checkWouldUserHaveScanPermission(null, false);
+ }
+
+ @Test
+ public void apply_permission_template_with_key_pattern_collision() {
+ final String key = "hi-test";
+ final String keyPattern = ".*-test";
+
+ Stream<PermissionTemplateDto> templates = Stream.of(
+ templateDb.insertTemplate(t -> t.setKeyPattern(keyPattern)),
+ templateDb.insertTemplate(t -> t.setKeyPattern(keyPattern))
+ );
+
+ String templateNames = templates
+ .map(PermissionTemplateDto::getName)
+ .sorted(String.CASE_INSENSITIVE_ORDER)
+ .map(x -> String.format("\"%s\"", x))
+ .collect(Collectors.joining(", "));
+
+ ProjectDto project = dbTester.components().insertPrivateProject(p -> p.setKey(key)).getProjectDto();
+
+ assertThatThrownBy(() -> underTest.applyDefaultToNewComponent(session, project, null))
+ .isInstanceOf(TemplateMatchingKeyException.class)
+ .hasMessageContaining("The \"%s\" key matches multiple permission templates: %s.", key, templateNames);
+ }
+
+ private void checkWouldUserHaveScanPermission(@Nullable String userUuid, boolean expectedResult) {
+ assertThat(underTest.wouldUserHaveScanPermissionWithDefaultTemplate(session, userUuid, "PROJECT_KEY"))
+ .isEqualTo(expectedResult);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/UserPermissionChangerIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/UserPermissionChangerIT.java
new file mode 100644
index 00000000000..479a28e81d4
--- /dev/null
+++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/permission/UserPermissionChangerIT.java
@@ -0,0 +1,346 @@
+/*
+ * 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.permission;
+
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.permission.PermissionServiceImpl;
+
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.server.common.permission.Operation.ADD;
+import static org.sonar.server.common.permission.Operation.REMOVE;
+import static org.sonar.server.permission.PermissionServiceImpl.ALL_PROJECT_PERMISSIONS;
+
+public class UserPermissionChangerIT {
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private final ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
+ private final PermissionService permissionService = new PermissionServiceImpl(resourceTypes);
+ private final UserPermissionChanger underTest = new UserPermissionChanger(db.getDbClient(), new SequenceUuidFactory());
+ private UserDto user1;
+ private UserDto user2;
+ private EntityDto privateProject;
+ private EntityDto publicProject;
+
+ @Before
+ public void setUp() {
+ user1 = db.users().insertUser();
+ user2 = db.users().insertUser();
+ privateProject = db.components().insertPrivateProject().getProjectDto();
+ publicProject = db.components().insertPublicProject().getProjectDto();
+ }
+
+ @Test
+ public void apply_adds_any_global_permission_to_user() {
+ permissionService.getGlobalPermissions()
+ .forEach(perm -> {
+ UserPermissionChange change = new UserPermissionChange(ADD, perm.getKey(), null, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).contains(perm);
+ });
+ }
+
+ @Test
+ public void apply_removes_any_global_permission_to_user() {
+ // give ADMIN perm to user2 so that user1 is not the only one with this permission and it can be removed from user1
+ db.users().insertGlobalPermissionOnUser(user2, GlobalPermission.ADMINISTER);
+ permissionService.getGlobalPermissions()
+ .forEach(perm -> db.users().insertGlobalPermissionOnUser(user1, perm));
+ assertThat(db.users().selectPermissionsOfUser(user1))
+ .containsOnly(permissionService.getGlobalPermissions().toArray(new GlobalPermission[0]));
+
+ permissionService.getGlobalPermissions()
+ .forEach(perm -> {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, perm.getKey(), null, UserIdDto.from(user1), permissionService);
+
+ apply(change, permissionService.getGlobalPermissions().stream().map(GlobalPermission::getKey).collect(toSet()));
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).doesNotContain(perm);
+ });
+ }
+
+ @Test
+ public void apply_has_no_effect_when_adding_permission_USER_on_a_public_project() {
+ UserPermissionChange change = new UserPermissionChange(ADD, UserRole.USER, publicProject, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, publicProject.getUuid())).doesNotContain(UserRole.USER);
+ }
+
+ @Test
+ public void apply_has_no_effect_when_adding_permission_CODEVIEWER_on_a_public_project() {
+ UserPermissionChange change = new UserPermissionChange(ADD, UserRole.CODEVIEWER, publicProject, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, publicProject.getUuid())).doesNotContain(UserRole.CODEVIEWER);
+ }
+
+ @Test
+ public void apply_adds_permission_ADMIN_on_a_public_project() {
+ applyAddsPermissionOnAPublicProject(UserRole.ADMIN);
+ }
+
+ @Test
+ public void apply_adds_permission_ISSUE_ADMIN_on_a_public_project() {
+ applyAddsPermissionOnAPublicProject(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_adds_permission_SCAN_EXECUTION_on_a_public_project() {
+ applyAddsPermissionOnAPublicProject(GlobalPermission.SCAN.getKey());
+ }
+
+ private void applyAddsPermissionOnAPublicProject(String permission) {
+ UserPermissionChange change = new UserPermissionChange(ADD, permission, publicProject, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, publicProject.getUuid())).containsOnly(permission);
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_permission_USER_from_a_public_project() {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, UserRole.USER, publicProject, UserIdDto.from(user1), permissionService);
+
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission user can't be removed from a public component");
+ }
+
+ @Test
+ public void apply_fails_with_BadRequestException_when_removing_permission_CODEVIEWER_from_a_public_project() {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, UserRole.CODEVIEWER, publicProject, UserIdDto.from(user1), permissionService);
+
+ assertThatThrownBy(() -> apply(change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Permission codeviewer can't be removed from a public component");
+ }
+
+ @Test
+ public void apply_removes_permission_ADMIN_from_a_public_project() {
+ applyRemovesPermissionFromPublicProject(UserRole.ADMIN);
+ }
+
+ @Test
+ public void apply_removes_permission_ISSUE_ADMIN_from_a_public_project() {
+ applyRemovesPermissionFromPublicProject(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void apply_removes_permission_SCAN_EXECUTION_from_a_public_project() {
+ applyRemovesPermissionFromPublicProject(GlobalPermission.SCAN.getKey());
+ }
+
+ private void applyRemovesPermissionFromPublicProject(String permission) {
+ db.users().insertProjectPermissionOnUser(user1, permission, publicProject);
+ UserPermissionChange change = new UserPermissionChange(REMOVE, permission, publicProject, UserIdDto.from(user1), permissionService);
+
+ apply(change, Set.of(permission));
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, publicProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void apply_adds_any_permission_to_a_private_project() {
+ permissionService.getAllProjectPermissions()
+ .forEach(permission -> {
+ UserPermissionChange change = new UserPermissionChange(ADD, permission, privateProject, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).contains(permission);
+ });
+ }
+
+ @Test
+ public void apply_removes_any_permission_from_a_private_project() {
+ permissionService.getAllProjectPermissions()
+ .forEach(permission -> db.users().insertProjectPermissionOnUser(user1, permission, privateProject));
+
+ permissionService.getAllProjectPermissions()
+ .forEach(permission -> {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, permission, privateProject, UserIdDto.from(user1), permissionService);
+
+ apply(change, ALL_PROJECT_PERMISSIONS);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).doesNotContain(permission);
+ });
+ }
+
+ @Test
+ public void add_global_permission_to_user() {
+ UserPermissionChange change = new UserPermissionChange(ADD, GlobalPermission.SCAN.getKey(), null, UserIdDto.from(user1), permissionService);
+
+ apply(change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).containsOnly(GlobalPermission.SCAN);
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).isEmpty();
+ assertThat(db.users().selectPermissionsOfUser(user2)).isEmpty();
+ assertThat(db.users().selectEntityPermissionOfUser(user2, privateProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void add_project_permission_to_user() {
+ UserPermissionChange change = new UserPermissionChange(ADD, UserRole.ISSUE_ADMIN, privateProject, UserIdDto.from(user1), permissionService);
+ apply(change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).isEmpty();
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).contains(UserRole.ISSUE_ADMIN);
+ assertThat(db.users().selectPermissionsOfUser(user2)).isEmpty();
+ assertThat(db.users().selectEntityPermissionOfUser(user2, privateProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void do_nothing_when_adding_global_permission_that_already_exists() {
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.ADMINISTER_QUALITY_GATES);
+
+ UserPermissionChange change = new UserPermissionChange(ADD, GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), null, UserIdDto.from(user1), permissionService);
+ apply(change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).containsOnly(GlobalPermission.ADMINISTER_QUALITY_GATES);
+ }
+
+ @Test
+ public void fail_to_add_global_permission_on_project() {
+ assertThatThrownBy(() -> {
+ UserPermissionChange change = new UserPermissionChange(ADD, GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), privateProject, UserIdDto.from(user1), permissionService);
+ apply(change);
+ })
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Invalid project permission 'gateadmin'. Valid values are [" + StringUtils.join(permissionService.getAllProjectPermissions(), ", ") + "]");
+ }
+
+ @Test
+ public void fail_to_add_project_permission() {
+ assertThatThrownBy(() -> {
+ UserPermissionChange change = new UserPermissionChange(ADD, UserRole.ISSUE_ADMIN, null, UserIdDto.from(user1), permissionService);
+ apply(change);
+ })
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Invalid global permission 'issueadmin'. Valid values are [admin, gateadmin, profileadmin, provisioning, scan]");
+ }
+
+ @Test
+ public void remove_global_permission_from_user() {
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.ADMINISTER_QUALITY_GATES);
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.SCAN);
+ db.users().insertGlobalPermissionOnUser(user2, GlobalPermission.ADMINISTER_QUALITY_GATES);
+ db.users().insertProjectPermissionOnUser(user1, UserRole.ISSUE_ADMIN, privateProject);
+
+ UserPermissionChange change = new UserPermissionChange(REMOVE, GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), null, UserIdDto.from(user1), permissionService);
+ apply(change, Set.of(GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), GlobalPermission.SCAN.getKey(), UserRole.ISSUE_ADMIN));
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).containsOnly(GlobalPermission.SCAN);
+ assertThat(db.users().selectPermissionsOfUser(user2)).containsOnly(GlobalPermission.ADMINISTER_QUALITY_GATES);
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).containsOnly(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void remove_project_permission_from_user() {
+ EntityDto project2 = db.components().insertPrivateProject().getProjectDto();
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.ADMINISTER_QUALITY_GATES);
+ db.users().insertProjectPermissionOnUser(user1, UserRole.ISSUE_ADMIN, privateProject);
+ db.users().insertProjectPermissionOnUser(user1, UserRole.USER, privateProject);
+ db.users().insertProjectPermissionOnUser(user2, UserRole.ISSUE_ADMIN, privateProject);
+ db.users().insertProjectPermissionOnUser(user1, UserRole.ISSUE_ADMIN, project2);
+
+ UserPermissionChange change = new UserPermissionChange(REMOVE, UserRole.ISSUE_ADMIN, privateProject, UserIdDto.from(user1), permissionService);
+ apply(change, Set.of(GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), UserRole.ISSUE_ADMIN, UserRole.USER));
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).containsOnly(UserRole.USER);
+ assertThat(db.users().selectEntityPermissionOfUser(user2, privateProject.getUuid())).containsOnly(UserRole.ISSUE_ADMIN);
+ assertThat(db.users().selectEntityPermissionOfUser(user1, project2.getUuid())).containsOnly(UserRole.ISSUE_ADMIN);
+ }
+
+ @Test
+ public void do_not_fail_if_removing_a_global_permission_that_does_not_exist() {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, GlobalPermission.ADMINISTER_QUALITY_GATES.getKey(), null, UserIdDto.from(user1), permissionService);
+ apply(change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).isEmpty();
+ }
+
+ @Test
+ public void do_not_fail_if_removing_a_project_permission_that_does_not_exist() {
+ UserPermissionChange change = new UserPermissionChange(REMOVE, UserRole.ISSUE_ADMIN, privateProject, UserIdDto.from(user1), permissionService);
+ apply(change);
+
+ assertThat(db.users().selectEntityPermissionOfUser(user1, privateProject.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void fail_to_remove_admin_global_permission_if_no_more_admins() {
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.ADMINISTER);
+
+ UserPermissionChange change = new UserPermissionChange(REMOVE, GlobalPermission.ADMINISTER.getKey(), null, UserIdDto.from(user1), permissionService);
+ DbSession session = db.getSession();
+ Set<String> permissions = Set.of(GlobalPermission.ADMINISTER.getKey());
+ assertThatThrownBy(() -> underTest.apply(session, permissions, change))
+ .isInstanceOf(BadRequestException.class)
+ .hasMessage("Last user with permission 'admin'. Permission cannot be removed.");
+ }
+
+ @Test
+ public void remove_admin_user_if_still_other_admins() {
+ db.users().insertGlobalPermissionOnUser(user1, GlobalPermission.ADMINISTER);
+ GroupDto admins = db.users().insertGroup("admins");
+ db.users().insertMember(admins, user2);
+ db.users().insertPermissionOnGroup(admins, GlobalPermission.ADMINISTER);
+
+ UserPermissionChange change = new UserPermissionChange(REMOVE, GlobalPermission.ADMINISTER.getKey(), null, UserIdDto.from(user1), permissionService);
+ underTest.apply(db.getSession(), Set.of(GlobalPermission.ADMINISTER.getKey()), change);
+
+ assertThat(db.users().selectPermissionsOfUser(user1)).isEmpty();
+ }
+
+ private void apply(UserPermissionChange change) {
+ underTest.apply(db.getSession(), Set.of(), change);
+ db.commit();
+ }
+ private void apply(UserPermissionChange change, Set<String> existingPermissions) {
+ underTest.apply(db.getSession(), existingPermissions, change);
+ db.commit();
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/ProjectKeyGenerator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/ProjectKeyGenerator.java
new file mode 100644
index 00000000000..cf5a04a2c8e
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/ProjectKeyGenerator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.almintegration;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.sonar.core.util.UuidFactory;
+
+import static com.google.common.collect.Lists.asList;
+import static org.sonar.core.component.ComponentKeys.sanitizeProjectKey;
+
+public class ProjectKeyGenerator {
+
+ @VisibleForTesting
+ static final int MAX_PROJECT_KEY_SIZE = 250;
+ @VisibleForTesting
+ static final Character PROJECT_KEY_SEPARATOR = '_';
+
+ private final UuidFactory uuidFactory;
+
+ public ProjectKeyGenerator(UuidFactory uuidFactory) {
+ this.uuidFactory = uuidFactory;
+ }
+
+ public String generateUniqueProjectKey(String projectName, String... extraProjectKeyItems) {
+ String sqProjectKey = generateCompleteProjectKey(projectName, extraProjectKeyItems);
+ sqProjectKey = truncateProjectKeyIfNecessary(sqProjectKey);
+ return sanitizeProjectKey(sqProjectKey);
+ }
+
+ private String generateCompleteProjectKey(String projectName, String[] extraProjectKeyItems) {
+ List<String> projectKeyItems = asList(projectName, extraProjectKeyItems);
+ String projectKey = StringUtils.join(projectKeyItems, PROJECT_KEY_SEPARATOR);
+ String uuid = uuidFactory.create();
+ return projectKey + PROJECT_KEY_SEPARATOR + uuid;
+ }
+
+ private static String truncateProjectKeyIfNecessary(String sqProjectKey) {
+ if (sqProjectKey.length() > MAX_PROJECT_KEY_SIZE) {
+ return sqProjectKey.substring(sqProjectKey.length() - MAX_PROJECT_KEY_SIZE);
+ }
+ return sqProjectKey;
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/package-info.java
new file mode 100644
index 00000000000..6cfcd9bc704
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almintegration/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.almintegration;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactory.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactory.java
new file mode 100644
index 00000000000..dc0660d97cf
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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.almsettings;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Priority;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+@ServerSide
+@Priority(1)
+public class DelegatingDevOpsProjectCreatorFactory implements DevOpsProjectCreatorFactory {
+
+ private final Set<DevOpsProjectCreatorFactory> delegates;
+
+ public DelegatingDevOpsProjectCreatorFactory(Set<DevOpsProjectCreatorFactory> delegates) {
+ this.delegates = delegates;
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(DbSession dbSession, Map<String, String> characteristics) {
+ return delegates.stream()
+ .flatMap(delegate -> delegate.getDevOpsProjectCreator(dbSession, characteristics).stream())
+ .findFirst();
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ return delegates.stream()
+ .flatMap(delegate -> delegate.getDevOpsProjectCreator(almSettingDto, devOpsProjectDescriptor).stream())
+ .findFirst();
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreator.java
new file mode 100644
index 00000000000..86225fa1ac8
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreator.java
@@ -0,0 +1,34 @@
+/*
+ * 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.almsettings;
+
+import javax.annotation.Nullable;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.server.component.ComponentCreationData;
+
+public interface DevOpsProjectCreator {
+
+ boolean isScanAllowedUsingPermissionsFromDevopsPlatform();
+
+ ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, CreationMethod creationMethod, Boolean monorepo, @Nullable String projectKey,
+ @Nullable String projectName);
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreatorFactory.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreatorFactory.java
new file mode 100644
index 00000000000..55835f45c23
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectCreatorFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.almsettings;
+
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+public interface DevOpsProjectCreatorFactory {
+
+ Optional<DevOpsProjectCreator> getDevOpsProjectCreator(DbSession dbSession, Map<String, String> characteristics);
+
+ Optional<DevOpsProjectCreator> getDevOpsProjectCreator(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor);
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectDescriptor.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectDescriptor.java
new file mode 100644
index 00000000000..4459ccbbf09
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/DevOpsProjectDescriptor.java
@@ -0,0 +1,25 @@
+/*
+ * 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.almsettings;
+
+import org.sonar.db.alm.setting.ALM;
+
+public record DevOpsProjectDescriptor(ALM alm, String url, String projectIdentifier) {
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreationParameters.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreationParameters.java
new file mode 100644
index 00000000000..72b2ca87602
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreationParameters.java
@@ -0,0 +1,32 @@
+/*
+ * 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.almsettings.github;
+
+import javax.annotation.Nullable;
+import org.sonar.auth.github.AppInstallationToken;
+import org.sonar.auth.github.security.AccessToken;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.user.UserSession;
+
+public record GithubProjectCreationParameters(DevOpsProjectDescriptor devOpsProjectDescriptor, AlmSettingDto almSettingDto, UserSession userSession,
+ AccessToken devOpsAppInstallationToken,
+ @Nullable AppInstallationToken authAppInstallationToken) {
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreator.java
new file mode 100644
index 00000000000..47902dd9c98
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreator.java
@@ -0,0 +1,229 @@
+/*
+ * 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.almsettings.github;
+
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.alm.client.github.GithubPermissionConverter;
+import org.sonar.api.web.UserRole;
+import org.sonar.auth.github.AppInstallationToken;
+import org.sonar.auth.github.GitHubSettings;
+import org.sonar.auth.github.GsonRepositoryCollaborator;
+import org.sonar.auth.github.GsonRepositoryPermissions;
+import org.sonar.auth.github.GsonRepositoryTeam;
+import org.sonar.auth.github.client.GithubApplicationClient;
+import org.sonar.auth.github.client.GithubApplicationClient.Repository;
+import org.sonar.auth.github.security.AccessToken;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.component.BranchDto;
+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.db.user.UserIdDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.permission.Operation;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.management.ManagedProjectService;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toSet;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class GithubProjectCreator implements DevOpsProjectCreator {
+
+ private final DbClient dbClient;
+ private final GithubApplicationClient githubApplicationClient;
+ private final GithubPermissionConverter githubPermissionConverter;
+ private final ProjectKeyGenerator projectKeyGenerator;
+ private final PermissionUpdater<UserPermissionChange> permissionUpdater;
+ private final PermissionService permissionService;
+ private final ManagedProjectService managedProjectService;
+ private final ProjectCreator projectCreator;
+ private final GithubProjectCreationParameters githubProjectCreationParameters;
+ private final DevOpsProjectDescriptor devOpsProjectDescriptor;
+ private final UserSession userSession;
+ private final AlmSettingDto almSettingDto;
+ private final AccessToken devOpsAppInstallationToken;
+ private final GitHubSettings gitHubSettings;
+
+ @CheckForNull
+ private final AppInstallationToken authAppInstallationToken;
+
+ public GithubProjectCreator(DbClient dbClient, GithubApplicationClient githubApplicationClient, GithubPermissionConverter githubPermissionConverter,
+ ProjectKeyGenerator projectKeyGenerator, PermissionUpdater<UserPermissionChange> permissionUpdater, PermissionService permissionService,
+ ManagedProjectService managedProjectService, ProjectCreator projectCreator, GithubProjectCreationParameters githubProjectCreationParameters, GitHubSettings gitHubSettings) {
+
+ this.dbClient = dbClient;
+ this.githubApplicationClient = githubApplicationClient;
+ this.githubPermissionConverter = githubPermissionConverter;
+ this.projectKeyGenerator = projectKeyGenerator;
+ this.permissionUpdater = permissionUpdater;
+ this.permissionService = permissionService;
+ this.managedProjectService = managedProjectService;
+ this.projectCreator = projectCreator;
+ this.githubProjectCreationParameters = githubProjectCreationParameters;
+ userSession = githubProjectCreationParameters.userSession();
+ almSettingDto = githubProjectCreationParameters.almSettingDto();
+ devOpsProjectDescriptor = githubProjectCreationParameters.devOpsProjectDescriptor();
+ devOpsAppInstallationToken = githubProjectCreationParameters.devOpsAppInstallationToken();
+ authAppInstallationToken = githubProjectCreationParameters.authAppInstallationToken();
+ this.gitHubSettings = gitHubSettings;
+ }
+
+ @Override
+ public boolean isScanAllowedUsingPermissionsFromDevopsPlatform() {
+ checkState(githubProjectCreationParameters.authAppInstallationToken() != null, "An auth app token is required in case repository permissions checking is necessary.");
+
+ String[] orgaAndRepoTokenified = devOpsProjectDescriptor.projectIdentifier().split("/");
+ String organization = orgaAndRepoTokenified[0];
+ String repository = orgaAndRepoTokenified[1];
+
+ Set<GithubPermissionsMappingDto> permissionsMappingDtos = dbClient.githubPermissionsMappingDao().findAll(dbClient.openSession(false));
+
+ boolean userHasDirectAccessToRepo = doesUserHaveScanPermission(organization, repository, permissionsMappingDtos);
+ if (userHasDirectAccessToRepo) {
+ return true;
+ }
+ return doesUserBelongToAGroupWithScanPermission(organization, repository, permissionsMappingDtos);
+ }
+
+ private boolean doesUserHaveScanPermission(String organization, String repository, Set<GithubPermissionsMappingDto> permissionsMappingDtos) {
+ Set<GsonRepositoryCollaborator> repositoryCollaborators = githubApplicationClient.getRepositoryCollaborators(devOpsProjectDescriptor.url(), authAppInstallationToken,
+ 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 organization, String repository,
+ Set<GithubPermissionsMappingDto> permissionsMappingDtos) {
+ Set<GsonRepositoryTeam> repositoryTeams = githubApplicationClient.getRepositoryTeams(devOpsProjectDescriptor.url(), authAppInstallationToken, organization, repository);
+
+ Set<String> groupsOfUser = findUserMembershipOnSonarQube(organization);
+ return repositoryTeams.stream()
+ .filter(team -> hasScanPermission(permissionsMappingDtos, team.permission(), team.permissions()))
+ .map(GsonRepositoryTeam::name)
+ .anyMatch(groupsOfUser::contains);
+ }
+
+ private Set<String> findUserMembershipOnSonarQube(String organization) {
+ return userSession.getGroups().stream()
+ .map(GroupDto::getName)
+ .filter(groupName -> groupName.contains("/"))
+ .map(name -> name.replaceFirst(organization + "/", ""))
+ .collect(toSet());
+ }
+
+ private boolean hasScanPermission(Set<GithubPermissionsMappingDto> permissionsMappingDtos, String role, GsonRepositoryPermissions permissions) {
+ Set<String> sonarqubePermissions = githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(permissionsMappingDtos,
+ role, permissions);
+ return sonarqubePermissions.contains(UserRole.SCAN);
+ }
+
+ @Override
+ public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, CreationMethod creationMethod, Boolean monorepo, @Nullable String projectKey,
+ @Nullable String projectName) {
+ String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
+ Repository repository = githubApplicationClient.getRepository(url, devOpsAppInstallationToken, devOpsProjectDescriptor.projectIdentifier())
+ .orElseThrow(() -> new IllegalStateException(
+ String.format("Impossible to find the repository '%s' on GitHub, using the devops config %s", devOpsProjectDescriptor.projectIdentifier(), almSettingDto.getKey())));
+
+ return createProjectAndBindToDevOpsPlatform(dbSession, monorepo, projectKey, projectName, almSettingDto, repository, creationMethod);
+ }
+
+ private ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, Boolean monorepo, @Nullable String projectKey, @Nullable String projectName,
+ AlmSettingDto almSettingDto,
+ Repository repository, CreationMethod creationMethod) {
+ String key = Optional.ofNullable(projectKey).orElse(getUniqueProjectKey(repository));
+
+ boolean isManaged = gitHubSettings.isProvisioningEnabled();
+
+ ComponentCreationData componentCreationData = projectCreator.createProject(dbSession, key, Optional.ofNullable(projectName).orElse(repository.getName()),
+ repository.getDefaultBranch(), creationMethod,
+ shouldProjectBePrivate(repository), isManaged);
+ ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
+ createProjectAlmSettingDto(dbSession, repository, projectDto, almSettingDto, monorepo);
+ addScanPermissionToCurrentUser(dbSession, projectDto);
+
+ BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();
+ if (gitHubSettings.isProvisioningEnabled()) {
+ syncProjectPermissionsWithGithub(projectDto, mainBranchDto);
+ }
+ return componentCreationData;
+ }
+
+ @CheckForNull
+ private Boolean shouldProjectBePrivate(Repository repository) {
+ if (gitHubSettings.isProvisioningEnabled() && gitHubSettings.isProjectVisibilitySynchronizationActivated()) {
+ return repository.isPrivate();
+ } else if (gitHubSettings.isProvisioningEnabled()) {
+ return true;
+ } else {
+ return null;
+ }
+ }
+
+ private void addScanPermissionToCurrentUser(DbSession dbSession, ProjectDto projectDto) {
+ UserIdDto userId = new UserIdDto(requireNonNull(userSession.getUuid()), requireNonNull(userSession.getLogin()));
+ UserPermissionChange scanPermission = new UserPermissionChange(Operation.ADD, UserRole.SCAN, projectDto, userId, permissionService);
+ permissionUpdater.apply(dbSession, Set.of(scanPermission));
+ }
+
+ private void syncProjectPermissionsWithGithub(ProjectDto projectDto, BranchDto mainBranchDto) {
+ String userUuid = requireNonNull(userSession.getUuid());
+ managedProjectService.queuePermissionSyncTask(userUuid, mainBranchDto.getUuid(), projectDto.getUuid());
+ }
+
+ private String getUniqueProjectKey(Repository repository) {
+ return projectKeyGenerator.generateUniqueProjectKey(repository.getFullName());
+ }
+
+ private void createProjectAlmSettingDto(DbSession dbSession, Repository repo, ProjectDto projectDto, AlmSettingDto almSettingDto, Boolean monorepo) {
+ ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
+ .setAlmSettingUuid(almSettingDto.getUuid())
+ .setAlmRepo(repo.getFullName())
+ .setAlmSlug(null)
+ .setProjectUuid(projectDto.getUuid())
+ .setSummaryCommentEnabled(true)
+ .setMonorepo(monorepo);
+ dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(), projectDto.getName(), projectDto.getKey());
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactory.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactory.java
new file mode 100644
index 00000000000..2a1821c2165
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactory.java
@@ -0,0 +1,174 @@
+/*
+ * 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.almsettings.github;
+
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.alm.client.github.GithubPermissionConverter;
+import org.sonar.api.server.ServerSide;
+import org.sonar.auth.github.AppInstallationToken;
+import org.sonar.auth.github.GitHubSettings;
+import org.sonar.auth.github.GithubAppConfiguration;
+import org.sonar.auth.github.client.GithubApplicationClient;
+import org.sonar.auth.github.security.AccessToken;
+import org.sonar.auth.github.security.UserAccessToken;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.exceptions.BadConfigurationException;
+import org.sonar.server.management.ManagedProjectService;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_PROJECT_IDENTIFIER;
+import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_URL;
+
+@ServerSide
+public class GithubProjectCreatorFactory implements DevOpsProjectCreatorFactory {
+ private static final Logger LOG = LoggerFactory.getLogger(GithubProjectCreatorFactory.class);
+
+ private final DbClient dbClient;
+ private final GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+ private final GithubApplicationClient githubApplicationClient;
+ private final ProjectKeyGenerator projectKeyGenerator;
+ private final ProjectCreator projectCreator;
+ private final UserSession userSession;
+ private final GitHubSettings gitHubSettings;
+ private final GithubPermissionConverter githubPermissionConverter;
+ private final PermissionUpdater<UserPermissionChange> permissionUpdater;
+ private final PermissionService permissionService;
+ private final ManagedProjectService managedProjectService;
+
+ public GithubProjectCreatorFactory(DbClient dbClient, GithubGlobalSettingsValidator githubGlobalSettingsValidator,
+ GithubApplicationClient githubApplicationClient, ProjectKeyGenerator projectKeyGenerator, UserSession userSession,
+ ProjectCreator projectCreator, GitHubSettings gitHubSettings, GithubPermissionConverter githubPermissionConverter,
+ PermissionUpdater<UserPermissionChange> permissionUpdater, PermissionService permissionService, ManagedProjectService managedProjectService) {
+ this.dbClient = dbClient;
+ this.githubGlobalSettingsValidator = githubGlobalSettingsValidator;
+ this.githubApplicationClient = githubApplicationClient;
+ this.projectKeyGenerator = projectKeyGenerator;
+ this.userSession = userSession;
+ this.projectCreator = projectCreator;
+ this.gitHubSettings = gitHubSettings;
+ this.githubPermissionConverter = githubPermissionConverter;
+ this.permissionUpdater = permissionUpdater;
+ this.permissionService = permissionService;
+ this.managedProjectService = managedProjectService;
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(DbSession dbSession, Map<String, String> characteristics) {
+ String githubApiUrl = characteristics.get(DEVOPS_PLATFORM_URL);
+ String githubRepository = characteristics.get(DEVOPS_PLATFORM_PROJECT_IDENTIFIER);
+ if (githubApiUrl == null || githubRepository == null) {
+ return Optional.empty();
+ }
+ DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, githubApiUrl, githubRepository);
+
+ return dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB).stream()
+ .filter(almSettingDto -> devOpsProjectDescriptor.url().equals(almSettingDto.getUrl()))
+ .map(almSettingDto -> findInstallationIdAndCreateDevOpsProjectCreator(devOpsProjectDescriptor, almSettingDto))
+ .flatMap(Optional::stream)
+ .findFirst();
+
+ }
+
+ private Optional<DevOpsProjectCreator> findInstallationIdAndCreateDevOpsProjectCreator(DevOpsProjectDescriptor devOpsProjectDescriptor,
+ AlmSettingDto almSettingDto) {
+ GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto);
+ return findInstallationIdToAccessRepo(githubAppConfiguration, devOpsProjectDescriptor.projectIdentifier())
+ .map(installationId -> generateAppInstallationToken(githubAppConfiguration, installationId))
+ .map(appInstallationToken -> createGithubProjectCreator(devOpsProjectDescriptor, almSettingDto, appInstallationToken));
+ }
+
+ private GithubProjectCreator createGithubProjectCreator(DevOpsProjectDescriptor devOpsProjectDescriptor, AlmSettingDto almSettingDto,
+ AppInstallationToken appInstallationToken) {
+ LOG.info("DevOps configuration {} auto-detected for project {}", almSettingDto.getKey(), devOpsProjectDescriptor.projectIdentifier());
+ Optional<AppInstallationToken> authAppInstallationToken = getAuthAppInstallationTokenIfNecessary(devOpsProjectDescriptor);
+
+ GithubProjectCreationParameters githubProjectCreationParameters = new GithubProjectCreationParameters(devOpsProjectDescriptor, almSettingDto, userSession, appInstallationToken,
+ authAppInstallationToken.orElse(null));
+ return new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator, permissionUpdater, permissionService,
+ managedProjectService, projectCreator, githubProjectCreationParameters, gitHubSettings);
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(AlmSettingDto almSettingDto,
+ DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ if (almSettingDto.getAlm() != ALM.GITHUB) {
+ return Optional.empty();
+ }
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ AccessToken accessToken = getAccessToken(dbSession, almSettingDto);
+ Optional<AppInstallationToken> authAppInstallationToken = getAuthAppInstallationTokenIfNecessary(devOpsProjectDescriptor);
+ GithubProjectCreationParameters githubProjectCreationParameters = new GithubProjectCreationParameters(devOpsProjectDescriptor, almSettingDto, userSession, accessToken,
+ authAppInstallationToken.orElse(null));
+ GithubProjectCreator githubProjectCreator = new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator, permissionUpdater,
+ permissionService, managedProjectService, this.projectCreator, githubProjectCreationParameters, gitHubSettings);
+ return Optional.of(githubProjectCreator);
+ }
+ }
+
+ private AccessToken getAccessToken(DbSession dbSession, AlmSettingDto almSettingDto) {
+ String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null.");
+ return dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto)
+ .map(AlmPatDto::getPersonalAccessToken)
+ .map(UserAccessToken::new)
+ .orElseThrow(() -> new IllegalArgumentException("No personal access token found"));
+ }
+
+ private Optional<AppInstallationToken> getAuthAppInstallationTokenIfNecessary(DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ if (gitHubSettings.isProvisioningEnabled()) {
+ GithubAppConfiguration githubAppConfiguration = new GithubAppConfiguration(Long.parseLong(gitHubSettings.appId()), gitHubSettings.privateKey(), gitHubSettings.apiURL());
+ long installationId = findInstallationIdToAccessRepo(githubAppConfiguration, devOpsProjectDescriptor.projectIdentifier())
+ .orElseThrow(() -> new BadConfigurationException("PROJECT",
+ format("GitHub auto-provisioning is activated. However the repo %s is not in the scope of the authentication application. "
+ + "The permissions can't be checked, and the project can not be created.",
+ devOpsProjectDescriptor.projectIdentifier())));
+ return Optional.of(generateAppInstallationToken(githubAppConfiguration, installationId));
+ }
+ return Optional.empty();
+ }
+
+ private Optional<Long> findInstallationIdToAccessRepo(GithubAppConfiguration githubAppConfiguration, String repositoryKey) {
+ return githubApplicationClient.getInstallationId(githubAppConfiguration, repositoryKey);
+ }
+
+ private AppInstallationToken generateAppInstallationToken(GithubAppConfiguration githubAppConfiguration, long installationId) {
+ return githubApplicationClient.createAppInstallationToken(githubAppConfiguration, installationId)
+ .orElseThrow(() -> new IllegalStateException(format("Error while generating token for GitHub Api Url %s (installation id: %s)",
+ githubAppConfiguration.getApiEndpoint(), installationId)));
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/package-info.java
new file mode 100644
index 00000000000..651c6710b55
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/github/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.almsettings.github;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreator.java
new file mode 100644
index 00000000000..2c530a0fc89
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreator.java
@@ -0,0 +1,145 @@
+/*
+ * 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.almsettings.gitlab;
+
+import java.util.Optional;
+import org.jetbrains.annotations.Nullable;
+import org.sonar.alm.client.gitlab.GitLabBranch;
+import org.sonar.alm.client.gitlab.GitlabApplicationClient;
+import org.sonar.alm.client.gitlab.GitlabServerException;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+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.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class GitlabProjectCreator implements DevOpsProjectCreator {
+
+ private final DbClient dbClient;
+ private final ProjectKeyGenerator projectKeyGenerator;
+ private final ProjectCreator projectCreator;
+ private final AlmSettingDto almSettingDto;
+ private final DevOpsProjectDescriptor devOpsProjectDescriptor;
+ private final GitlabApplicationClient gitlabApplicationClient;
+ private final UserSession userSession;
+
+ public GitlabProjectCreator(DbClient dbClient, ProjectKeyGenerator projectKeyGenerator, ProjectCreator projectCreator, AlmSettingDto almSettingDto,
+ DevOpsProjectDescriptor devOpsProjectDescriptor, GitlabApplicationClient gitlabApplicationClient, UserSession userSession) {
+ this.dbClient = dbClient;
+ this.projectKeyGenerator = projectKeyGenerator;
+ this.projectCreator = projectCreator;
+ this.almSettingDto = almSettingDto;
+ this.devOpsProjectDescriptor = devOpsProjectDescriptor;
+ this.gitlabApplicationClient = gitlabApplicationClient;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public boolean isScanAllowedUsingPermissionsFromDevopsPlatform() {
+ throw new UnsupportedOperationException("Not Implemented");
+ }
+
+ @Override
+ public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, CreationMethod creationMethod, Boolean monorepo, @Nullable String projectKey,
+ @Nullable String projectName) {
+
+ String pat = findPersonalAccessTokenOrThrow(dbSession, almSettingDto);
+
+ String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "DevOps Platform gitlabUrl cannot be null");
+
+ Long gitlabProjectId = getGitlabProjectId();
+ Project gitlabProject = fetchGitlabProject(gitlabUrl, pat, gitlabProjectId);
+
+ Optional<String> almDefaultBranch = getDefaultBranchOnGitlab(gitlabUrl, pat, gitlabProjectId);
+ ComponentCreationData componentCreationData = projectCreator.createProject(
+ dbSession,
+ getProjectKey(projectKey, gitlabProject),
+ getProjectName(projectName, gitlabProject),
+ almDefaultBranch.orElse(null),
+ creationMethod);
+ ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
+
+ createProjectAlmSettingDto(dbSession, gitlabProjectId.toString(), projectDto, almSettingDto, monorepo);
+ return componentCreationData;
+ }
+
+ private String findPersonalAccessTokenOrThrow(DbSession dbSession, AlmSettingDto almSettingDto) {
+ String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null.");
+ Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
+ return almPatDto.map(AlmPatDto::getPersonalAccessToken)
+ .orElseThrow(() -> new IllegalArgumentException(format("personal access token for '%s' is missing", almSettingDto.getKey())));
+ }
+
+ private Long getGitlabProjectId() {
+ try {
+ return Long.parseLong(devOpsProjectDescriptor.projectIdentifier());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(format("GitLab project identifier must be a number, was '%s'", devOpsProjectDescriptor.projectIdentifier()));
+ }
+ }
+
+ private Project fetchGitlabProject(String gitlabUrl, String pat, Long gitlabProjectId) {
+ try {
+ return gitlabApplicationClient.getProject(
+ gitlabUrl,
+ pat,
+ gitlabProjectId);
+ } catch (GitlabServerException e) {
+ throw new IllegalStateException(format("Failed to fetch GitLab project with ID '%s' from '%s'", gitlabProjectId, gitlabUrl), e);
+ }
+ }
+
+ private Optional<String> getDefaultBranchOnGitlab(String gitlabUrl, String pat, long gitlabProjectId) {
+ Optional<GitLabBranch> almMainBranch = gitlabApplicationClient.getBranches(gitlabUrl, pat, gitlabProjectId).stream().filter(GitLabBranch::isDefault).findFirst();
+ return almMainBranch.map(GitLabBranch::getName);
+ }
+
+ private String getProjectKey(@Nullable String projectKey, Project gitlabProject) {
+ return Optional.ofNullable(projectKey).orElseGet(() -> projectKeyGenerator.generateUniqueProjectKey(gitlabProject.getPathWithNamespace()));
+ }
+
+ private static String getProjectName(@Nullable String projectName, Project gitlabProject) {
+ return Optional.ofNullable(projectName).orElse(gitlabProject.getName());
+ }
+
+ private void createProjectAlmSettingDto(DbSession dbSession, String gitlabProjectId, ProjectDto projectDto, AlmSettingDto almSettingDto, Boolean monorepo) {
+ ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
+ .setAlmSettingUuid(almSettingDto.getUuid())
+ .setAlmRepo(gitlabProjectId)
+ .setAlmSlug(null)
+ .setProjectUuid(projectDto.getUuid())
+ .setSummaryCommentEnabled(true)
+ .setMonorepo(monorepo);
+ dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(), projectDto.getName(), projectDto.getKey());
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactory.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactory.java
new file mode 100644
index 00000000000..19176e8c41a
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactory.java
@@ -0,0 +1,72 @@
+/*
+ * 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.almsettings.gitlab;
+
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.alm.client.gitlab.GitlabApplicationClient;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.user.UserSession;
+
+public class GitlabProjectCreatorFactory implements DevOpsProjectCreatorFactory {
+ private final DbClient dbClient;
+ private final ProjectKeyGenerator projectKeyGenerator;
+ private final ProjectCreator projectCreator;
+ private final GitlabApplicationClient gitlabApplicationClient;
+ private final UserSession userSession;
+
+ public GitlabProjectCreatorFactory(DbClient dbClient, ProjectKeyGenerator projectKeyGenerator, ProjectCreator projectCreator, GitlabApplicationClient gitlabApplicationClient,
+ UserSession userSession) {
+ this.dbClient = dbClient;
+ this.projectKeyGenerator = projectKeyGenerator;
+ this.projectCreator = projectCreator;
+ this.gitlabApplicationClient = gitlabApplicationClient;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(DbSession dbSession, Map<String, String> characteristics) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ if (almSettingDto.getAlm() != ALM.GITLAB) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new GitlabProjectCreator(
+ dbClient,
+ projectKeyGenerator,
+ projectCreator,
+ almSettingDto,
+ devOpsProjectDescriptor,
+ gitlabApplicationClient,
+ userSession));
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/package-info.java
new file mode 100644
index 00000000000..f5cc5a1abc8
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/gitlab/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.almsettings.gitlab;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/package-info.java
new file mode 100644
index 00000000000..2883960112a
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/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.almsettings;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentCreationParameters.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentCreationParameters.java
new file mode 100644
index 00000000000..4ba5d2c8431
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentCreationParameters.java
@@ -0,0 +1,78 @@
+/*
+ * 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.component;
+
+import javax.annotation.Nullable;
+import org.sonar.db.project.CreationMethod;
+
+public record ComponentCreationParameters(NewComponent newComponent,
+ @Nullable String userUuid,
+ @Nullable String userLogin,
+ @Nullable String mainBranchName,
+ boolean isManaged,
+ CreationMethod creationMethod) {
+
+ public static ProjectCreationDataBuilder builder() {
+ return new ProjectCreationDataBuilder();
+ }
+
+ public static final class ProjectCreationDataBuilder {
+ private NewComponent newComponent;
+ private String userUuid = null;
+ private String userLogin = null;
+ private String mainBranchName = null;
+ private boolean isManaged = false;
+ private CreationMethod creationMethod;
+
+ public ProjectCreationDataBuilder newComponent(NewComponent newComponent) {
+ this.newComponent = newComponent;
+ return this;
+ }
+
+ public ProjectCreationDataBuilder userUuid(@Nullable String userUuid) {
+ this.userUuid = userUuid;
+ return this;
+ }
+
+ public ProjectCreationDataBuilder userLogin(@Nullable String userLogin) {
+ this.userLogin = userLogin;
+ return this;
+ }
+
+ public ProjectCreationDataBuilder mainBranchName(@Nullable String mainBranchName) {
+ this.mainBranchName = mainBranchName;
+ return this;
+ }
+
+ public ProjectCreationDataBuilder isManaged(boolean isManaged) {
+ this.isManaged = isManaged;
+ return this;
+ }
+
+ public ProjectCreationDataBuilder creationMethod(CreationMethod creationMethod) {
+ this.creationMethod = creationMethod;
+ return this;
+ }
+
+ public ComponentCreationParameters build() {
+ return new ComponentCreationParameters(newComponent, userUuid, userLogin, mainBranchName, isManaged, creationMethod);
+ }
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java
new file mode 100644
index 00000000000..e42e77a33e1
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/ComponentUpdater.java
@@ -0,0 +1,259 @@
+/*
+ * 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.component;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.System2;
+import org.sonar.core.i18n.I18n;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.portfolio.PortfolioDto;
+import org.sonar.db.portfolio.PortfolioDto.SelectionMode;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.permission.Operation;
+import org.sonar.server.common.permission.PermissionTemplateService;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.es.Indexers;
+import org.sonar.server.favorite.FavoriteUpdater;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.project.DefaultBranchNameResolver;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Collections.singletonList;
+import static org.sonar.api.web.UserRole.PUBLIC_PERMISSIONS;
+import static org.sonar.core.component.ComponentKeys.ALLOWED_CHARACTERS_MESSAGE;
+import static org.sonar.core.component.ComponentKeys.isValidProjectKey;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
+
+public class ComponentUpdater {
+
+ private static final Set<String> PROJ_APP_QUALIFIERS = Set.of(Qualifiers.PROJECT, Qualifiers.APP);
+ private static final String KEY_ALREADY_EXISTS_ERROR = "Could not create %s with key: \"%s\". A similar key already exists: \"%s\"";
+ private static final String MALFORMED_KEY_ERROR = "Malformed key for %s: '%s'. %s.";
+ private final DbClient dbClient;
+ private final I18n i18n;
+ private final System2 system2;
+ private final PermissionTemplateService permissionTemplateService;
+ private final FavoriteUpdater favoriteUpdater;
+ private final Indexers indexers;
+ private final UuidFactory uuidFactory;
+ private final DefaultBranchNameResolver defaultBranchNameResolver;
+ private final PermissionUpdater<UserPermissionChange> userPermissionUpdater;
+ private final PermissionService permissionService;
+
+ public ComponentUpdater(DbClient dbClient, I18n i18n, System2 system2,
+ PermissionTemplateService permissionTemplateService, FavoriteUpdater favoriteUpdater,
+ Indexers indexers, UuidFactory uuidFactory, DefaultBranchNameResolver defaultBranchNameResolver, PermissionUpdater<UserPermissionChange> userPermissionUpdater,
+ PermissionService permissionService) {
+ this.dbClient = dbClient;
+ this.i18n = i18n;
+ this.system2 = system2;
+ this.permissionTemplateService = permissionTemplateService;
+ this.favoriteUpdater = favoriteUpdater;
+ this.indexers = indexers;
+ this.uuidFactory = uuidFactory;
+ this.defaultBranchNameResolver = defaultBranchNameResolver;
+ this.userPermissionUpdater = userPermissionUpdater;
+ this.permissionService = permissionService;
+ }
+
+ /**
+ * - Create component
+ * - Apply default permission template
+ * - Add component to favorite if the component has the 'Project Creators' permission
+ * - Index component in es indexes
+ */
+ public ComponentCreationData create(DbSession dbSession, ComponentCreationParameters componentCreationParameters) {
+ ComponentCreationData componentCreationData = createWithoutCommit(dbSession, componentCreationParameters);
+ commitAndIndex(dbSession, componentCreationData);
+ return componentCreationData;
+ }
+
+ public void commitAndIndex(DbSession dbSession, ComponentCreationData componentCreationData) {
+ if (componentCreationData.portfolioDto() != null) {
+ indexers.commitAndIndexEntities(dbSession, singletonList(componentCreationData.portfolioDto()), Indexers.EntityEvent.CREATION);
+ } else if (componentCreationData.projectDto() != null) {
+ indexers.commitAndIndexEntities(dbSession, singletonList(componentCreationData.projectDto()), Indexers.EntityEvent.CREATION);
+ }
+ }
+
+ /**
+ * Create component without committing.
+ * Don't forget to call commitAndIndex(...) when ready to commit.
+ */
+ public ComponentCreationData createWithoutCommit(DbSession dbSession, ComponentCreationParameters componentCreationParameters) {
+ checkKeyFormat(componentCreationParameters.newComponent().qualifier(), componentCreationParameters.newComponent().key());
+ checkKeyAlreadyExists(dbSession, componentCreationParameters.newComponent());
+
+ long now = system2.now();
+
+ ComponentDto componentDto = createRootComponent(dbSession, componentCreationParameters.newComponent(), now);
+
+ BranchDto mainBranch = null;
+ ProjectDto projectDto = null;
+ PortfolioDto portfolioDto = null;
+
+ if (isProjectOrApp(componentDto)) {
+ projectDto = toProjectDto(componentDto, now, componentCreationParameters.creationMethod());
+ dbClient.projectDao().insert(dbSession, projectDto);
+ addToFavourites(dbSession, projectDto, componentCreationParameters.userUuid(), componentCreationParameters.userLogin());
+ mainBranch = createMainBranch(dbSession, componentDto.uuid(), projectDto.getUuid(), componentCreationParameters.mainBranchName());
+ if (componentCreationParameters.isManaged()) {
+ applyPublicPermissionsForCreator(dbSession, projectDto, componentCreationParameters.userUuid());
+ } else {
+ permissionTemplateService.applyDefaultToNewComponent(dbSession, projectDto, componentCreationParameters.userUuid());
+ }
+ } else if (isPortfolio(componentDto)) {
+ portfolioDto = toPortfolioDto(componentDto, now);
+ dbClient.portfolioDao().insert(dbSession, portfolioDto, false);
+ permissionTemplateService.applyDefaultToNewComponent(dbSession, portfolioDto, componentCreationParameters.userUuid());
+ } else {
+ throw new IllegalArgumentException("Component " + componentDto + " is not a top level entity");
+ }
+
+ return new ComponentCreationData(componentDto, portfolioDto, mainBranch, projectDto);
+ }
+
+ private void applyPublicPermissionsForCreator(DbSession dbSession, ProjectDto projectDto, @Nullable String userUuid) {
+ if (userUuid != null) {
+ UserDto userDto = dbClient.userDao().selectByUuid(dbSession, userUuid);
+ checkState(userDto != null, "User with uuid '%s' doesn't exist", userUuid);
+ userPermissionUpdater.apply(dbSession,
+ PUBLIC_PERMISSIONS.stream()
+ .map(permission -> toUserPermissionChange(permission, projectDto, userDto))
+ .collect(Collectors.toSet()));
+ }
+ }
+
+ private UserPermissionChange toUserPermissionChange(String permission, ProjectDto projectDto, UserDto userDto) {
+ return new UserPermissionChange(Operation.ADD, permission, projectDto, userDto, permissionService);
+ }
+
+ private void addToFavourites(DbSession dbSession, ProjectDto projectDto, @Nullable String userUuid, @Nullable String userLogin) {
+ if (permissionTemplateService.hasDefaultTemplateWithPermissionOnProjectCreator(dbSession, projectDto)) {
+ favoriteUpdater.add(dbSession, projectDto, userUuid, userLogin, false);
+ }
+ }
+
+ private void checkKeyFormat(String qualifier, String key) {
+ checkRequest(isValidProjectKey(key), MALFORMED_KEY_ERROR, getQualifierToDisplay(qualifier), key, ALLOWED_CHARACTERS_MESSAGE);
+ }
+
+ private void checkKeyAlreadyExists(DbSession dbSession, NewComponent newComponent) {
+ List<ComponentDto> componentDtos = dbClient.componentDao().selectByKeyCaseInsensitive(dbSession, newComponent.key());
+
+ if (!componentDtos.isEmpty()) {
+ String alreadyExistingKeys = componentDtos
+ .stream()
+ .map(ComponentDto::getKey)
+ .collect(Collectors.joining(", "));
+ throwBadRequestException(KEY_ALREADY_EXISTS_ERROR, getQualifierToDisplay(newComponent.qualifier()), newComponent.key(), alreadyExistingKeys);
+ }
+ }
+
+ private ComponentDto createRootComponent(DbSession session, NewComponent newComponent, long now) {
+ String uuid = uuidFactory.create();
+
+ ComponentDto component = new ComponentDto()
+ .setUuid(uuid)
+ .setUuidPath(ComponentDto.UUID_PATH_OF_ROOT)
+ .setBranchUuid(uuid)
+ .setKey(newComponent.key())
+ .setName(newComponent.name())
+ .setDescription(newComponent.description())
+ .setLongName(newComponent.name())
+ .setScope(Scopes.PROJECT)
+ .setQualifier(newComponent.qualifier())
+ .setPrivate(newComponent.isPrivate())
+ .setCreatedAt(new Date(now));
+
+ dbClient.componentDao().insert(session, component, true);
+ return component;
+ }
+
+ private ProjectDto toProjectDto(ComponentDto component, long now, CreationMethod creationMethod) {
+ return new ProjectDto()
+ .setUuid(uuidFactory.create())
+ .setKey(component.getKey())
+ .setQualifier(component.qualifier())
+ .setName(component.name())
+ .setPrivate(component.isPrivate())
+ .setDescription(component.description())
+ .setCreationMethod(creationMethod)
+ .setUpdatedAt(now)
+ .setCreatedAt(now);
+ }
+
+ private static PortfolioDto toPortfolioDto(ComponentDto component, long now) {
+ return new PortfolioDto()
+ .setUuid(component.uuid())
+ .setRootUuid(component.branchUuid())
+ .setKey(component.getKey())
+ .setName(component.name())
+ .setPrivate(component.isPrivate())
+ .setDescription(component.description())
+ .setSelectionMode(SelectionMode.NONE.name())
+ .setUpdatedAt(now)
+ .setCreatedAt(now);
+ }
+
+ private static boolean isProjectOrApp(ComponentDto componentDto) {
+ return PROJ_APP_QUALIFIERS.contains(componentDto.qualifier());
+ }
+
+ private static boolean isPortfolio(ComponentDto componentDto) {
+ return Qualifiers.VIEW.contains(componentDto.qualifier());
+ }
+
+ private BranchDto createMainBranch(DbSession session, String componentUuid, String projectUuid, @Nullable String mainBranch) {
+ BranchDto branch = new BranchDto()
+ .setBranchType(BranchType.BRANCH)
+ .setUuid(componentUuid)
+ .setIsMain(true)
+ .setKey(Optional.ofNullable(mainBranch).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()))
+ .setMergeBranchUuid(null)
+ .setExcludeFromPurge(true)
+ .setProjectUuid(projectUuid);
+ dbClient.branchDao().upsert(session, branch);
+ return branch;
+ }
+
+ private String getQualifierToDisplay(String qualifier) {
+ return i18n.message(Locale.getDefault(), "qualifier." + qualifier, "Project");
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/NewComponent.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/NewComponent.java
new file mode 100644
index 00000000000..8167bdae6ab
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/NewComponent.java
@@ -0,0 +1,120 @@
+/*
+ * 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.component;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.db.component.ComponentValidator.checkComponentKey;
+import static org.sonar.db.component.ComponentValidator.checkComponentName;
+import static org.sonar.db.component.ComponentValidator.checkComponentQualifier;
+
+@Immutable
+public class NewComponent {
+ private final String key;
+ private final String qualifier;
+ private final String name;
+ private final String description;
+ private final boolean isPrivate;
+
+ private NewComponent(NewComponent.Builder builder) {
+ this.key = builder.key;
+ this.qualifier = builder.qualifier;
+ this.name = builder.name;
+ this.isPrivate = builder.isPrivate;
+ this.description = builder.description;
+ }
+
+ public static Builder newComponentBuilder() {
+ return new Builder();
+ }
+
+ public String key() {
+ return key;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String qualifier() {
+ return qualifier;
+ }
+
+ public boolean isPrivate() {
+ return isPrivate;
+ }
+
+ @CheckForNull
+ public String description() {
+ return description;
+ }
+
+ public boolean isProject() {
+ return PROJECT.equals(qualifier);
+ }
+
+ public static class Builder {
+ private String description;
+ private String key;
+ private String qualifier = PROJECT;
+ private String name;
+ private boolean isPrivate = false;
+
+ private Builder() {
+ // use static factory method newComponentBuilder()
+ }
+
+ public Builder setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public Builder setQualifier(String qualifier) {
+ this.qualifier = qualifier;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setPrivate(boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ return this;
+ }
+
+ public Builder setDescription(@Nullable String description) {
+ this.description = description;
+ return this;
+ }
+
+ public NewComponent build() {
+ checkComponentKey(key);
+ checkComponentName(name);
+ checkComponentQualifier(qualifier);
+ return new NewComponent(this);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/package-info.java
new file mode 100644
index 00000000000..51d90d85a8c
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/component/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.component;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/CaycUtils.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/CaycUtils.java
new file mode 100644
index 00000000000..b206539df16
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/CaycUtils.java
@@ -0,0 +1,39 @@
+/*
+ * 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.newcodeperiod;
+
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+public interface CaycUtils {
+ static boolean isNewCodePeriodCompliant(NewCodePeriodType type, String value) {
+ if (type == NewCodePeriodType.NUMBER_OF_DAYS) {
+ return parseDays(value) > 0 && parseDays(value) <= 90;
+ }
+ return true;
+ }
+
+ static int parseDays(String value) {
+ try {
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to parse number of days: " + value);
+ }
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolver.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolver.java
new file mode 100644
index 00000000000..8956831e8cf
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolver.java
@@ -0,0 +1,149 @@
+/*
+ * 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.newcodeperiod;
+
+import com.google.common.base.Preconditions;
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.platform.PlatformEditionProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodParser;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+
+public class NewCodeDefinitionResolver {
+ private static final String BEGIN_LIST = "<ul>";
+
+ private static final String END_LIST = "</ul>";
+ private static final String BEGIN_ITEM_LIST = "<li>";
+ private static final String END_ITEM_LIST = "</li>";
+
+ public static final String NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION = "Project New Code Definition Type<br/>" +
+ "New code definitions of the following types are allowed:" +
+ BEGIN_LIST +
+ BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + END_ITEM_LIST +
+ BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
+ BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - will default to the main branch." + END_ITEM_LIST +
+ END_LIST;
+
+ public static final String NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION = "Project New Code Definition Value<br/>" +
+ "For each new code definition type, a different value is expected:" +
+ BEGIN_LIST +
+ BEGIN_ITEM_LIST + "no value, when the new code definition type is " + PREVIOUS_VERSION.name() + " and " + REFERENCE_BRANCH.name() + END_ITEM_LIST +
+ BEGIN_ITEM_LIST + "a number between 1 and 90, when the new code definition type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
+ END_LIST;
+
+ private static final String UNEXPECTED_VALUE_ERROR_MESSAGE = "Unexpected value for newCodeDefinitionType '%s'";
+
+ private static final EnumSet<NewCodePeriodType> projectCreationNCDTypes = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH);
+
+ private final DbClient dbClient;
+ private final PlatformEditionProvider editionProvider;
+
+ public NewCodeDefinitionResolver(DbClient dbClient, PlatformEditionProvider editionProvider) {
+ this.dbClient = dbClient;
+ this.editionProvider = editionProvider;
+ }
+
+ public void createNewCodeDefinition(DbSession dbSession, String projectUuid, String mainBranchUuid,
+ String defaultBranchName, String newCodeDefinitionType, @Nullable String newCodeDefinitionValue) {
+
+ boolean isCommunityEdition = editionProvider.get().filter(EditionProvider.Edition.COMMUNITY::equals).isPresent();
+ NewCodePeriodType newCodePeriodType = parseNewCodeDefinitionType(newCodeDefinitionType);
+
+ NewCodePeriodDto dto = new NewCodePeriodDto();
+ dto.setType(newCodePeriodType);
+ dto.setProjectUuid(projectUuid);
+
+ if (isCommunityEdition) {
+ dto.setBranchUuid(mainBranchUuid);
+ }
+
+ getNewCodeDefinitionValueProjectCreation(newCodePeriodType, newCodeDefinitionValue, defaultBranchName).ifPresent(dto::setValue);
+
+ if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) {
+ throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. "
+ + "Please refer to the documentation for compliant options.");
+ }
+
+ dbClient.newCodePeriodDao().insert(dbSession, dto);
+ }
+
+ public static void checkNewCodeDefinitionParam(@Nullable String newCodeDefinitionType, @Nullable String newCodeDefinitionValue) {
+ if (newCodeDefinitionType == null && newCodeDefinitionValue != null) {
+ throw new IllegalArgumentException("New code definition type is required when new code definition value is provided");
+ }
+ }
+
+ private static Optional<String> getNewCodeDefinitionValueProjectCreation(NewCodePeriodType type, @Nullable String value, String defaultBranchName) {
+ return switch (type) {
+ case PREVIOUS_VERSION -> {
+ Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type);
+ yield Optional.empty();
+ }
+ case NUMBER_OF_DAYS -> {
+ requireValue(type, value);
+ yield Optional.of(parseDays(value));
+ }
+ case REFERENCE_BRANCH -> {
+ Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type);
+ yield Optional.of(defaultBranchName);
+ }
+ default -> throw new IllegalStateException("Unexpected type: " + type);
+ };
+ }
+
+ private static String parseDays(String value) {
+ try {
+ return Integer.toString(NewCodePeriodParser.parseDays(value));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to parse number of days: " + value);
+ }
+ }
+
+ private static void requireValue(NewCodePeriodType type, @Nullable String value) {
+ Preconditions.checkArgument(value != null, "New code definition type '%s' requires a newCodeDefinitionValue", type);
+ }
+
+ private static NewCodePeriodType parseNewCodeDefinitionType(String typeStr) {
+ NewCodePeriodType type;
+ try {
+ type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid type: " + typeStr);
+ }
+ validateType(type);
+ return type;
+ }
+
+ private static void validateType(NewCodePeriodType type) {
+ Preconditions.checkArgument(projectCreationNCDTypes.contains(type), "Invalid type '%s'. `newCodeDefinitionType` can only be set with types: %s",
+ type, projectCreationNCDTypes);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/package-info.java
new file mode 100644
index 00000000000..47a06a53a29
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/newcodeperiod/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.newcodeperiod;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolver.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolver.java
new file mode 100644
index 00000000000..471d49723bf
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolver.java
@@ -0,0 +1,68 @@
+/*
+ * 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.permission;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.db.DbSession;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+public interface DefaultTemplatesResolver {
+ /**
+ * Resolve the effective default templates uuid for the specified {@link DefaultTemplates}.
+ * <ul>
+ * <li>{@link ResolvedDefaultTemplates#project} is always the same as {@link DefaultTemplates#projectUuid}</li>
+ * <li>when Governance is not installed, {@link ResolvedDefaultTemplates#application} is always {@code null}</li>
+ * <li>when Governance is installed, {@link ResolvedDefaultTemplates#application} is {@link DefaultTemplates#applicationsUuid}
+ * when it is non {@code null}, otherwise it is {@link DefaultTemplates#projectUuid}</li>
+ * <li>when Governance is installed, {@link ResolvedDefaultTemplates#portfolio} is {@link DefaultTemplates#portfoliosUuid}
+ * when it is non {@code null}, otherwise it is {@link DefaultTemplates#projectUuid}</li>
+ * </ul>
+ */
+ ResolvedDefaultTemplates resolve(DbSession dbSession);
+
+ @Immutable
+ final class ResolvedDefaultTemplates {
+ private final String project;
+ private final String application;
+ private final String portfolio;
+
+ public ResolvedDefaultTemplates(String project, @Nullable String application, @Nullable String portfolio) {
+ this.project = requireNonNull(project, "project can't be null");
+ this.application = application;
+ this.portfolio = portfolio;
+ }
+
+ public String getProject() {
+ return project;
+ }
+
+ public Optional<String> getApplication() {
+ return ofNullable(application);
+ }
+
+ public Optional<String> getPortfolio() {
+ return ofNullable(portfolio);
+ }
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolverImpl.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolverImpl.java
new file mode 100644
index 00000000000..ec32b6d3cf6
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/DefaultTemplatesResolverImpl.java
@@ -0,0 +1,71 @@
+/*
+ * 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.permission;
+
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceType;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.property.InternalProperties;
+
+public class DefaultTemplatesResolverImpl implements DefaultTemplatesResolver {
+
+ private final DbClient dbClient;
+ private final ResourceTypes resourceTypes;
+
+ public DefaultTemplatesResolverImpl(DbClient dbClient, ResourceTypes resourceTypes) {
+ this.dbClient = dbClient;
+ this.resourceTypes = resourceTypes;
+ }
+
+ @Override
+ public ResolvedDefaultTemplates resolve(DbSession dbSession) {
+ String defaultProjectTemplate = dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.DEFAULT_PROJECT_TEMPLATE).orElseThrow(() -> {
+ throw new IllegalStateException("Default template for project is missing");
+ });
+
+ String defaultPortfolioTemplate = null;
+ String defaultApplicationTemplate = null;
+
+ if (isPortfolioEnabled(resourceTypes)) {
+ defaultPortfolioTemplate = dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.DEFAULT_PORTFOLIO_TEMPLATE).orElse(defaultProjectTemplate);
+ }
+ if (isApplicationEnabled(resourceTypes)) {
+ defaultApplicationTemplate = dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.DEFAULT_APPLICATION_TEMPLATE).orElse(defaultProjectTemplate);
+ }
+ return new ResolvedDefaultTemplates(defaultProjectTemplate, defaultApplicationTemplate, defaultPortfolioTemplate);
+ }
+
+ private static boolean isPortfolioEnabled(ResourceTypes resourceTypes) {
+ return resourceTypes.getRoots()
+ .stream()
+ .map(ResourceType::getQualifier)
+ .anyMatch(Qualifiers.VIEW::equals);
+ }
+
+ private static boolean isApplicationEnabled(ResourceTypes resourceTypes) {
+ return resourceTypes.getRoots()
+ .stream()
+ .map(ResourceType::getQualifier)
+ .anyMatch(Qualifiers.APP::equals);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GranteeTypeSpecificPermissionUpdater.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GranteeTypeSpecificPermissionUpdater.java
new file mode 100644
index 00000000000..76a7602a91d
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GranteeTypeSpecificPermissionUpdater.java
@@ -0,0 +1,32 @@
+/*
+ * 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.permission;
+
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.db.DbSession;
+
+public interface GranteeTypeSpecificPermissionUpdater<T extends PermissionChange> {
+ Class<T> getHandledClass();
+
+ Set<String> loadExistingEntityPermissions(DbSession dbSession, String uuidOfGrantee, @Nullable String entityUuid);
+
+ boolean apply(DbSession dbSession, Set<String> existingPermissions, T change);
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChange.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChange.java
new file mode 100644
index 00000000000..cb655361bdf
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChange.java
@@ -0,0 +1,53 @@
+/*
+ * 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.permission;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.permission.GroupUuidOrAnyone;
+import org.sonar.server.permission.PermissionService;
+
+public class GroupPermissionChange extends PermissionChange {
+
+ private final GroupDto groupDto;
+
+ public GroupPermissionChange(Operation operation, String permission, @Nullable EntityDto entityDto,
+ @Nullable GroupDto groupDto, PermissionService permissionService) {
+ super(operation, permission, entityDto, permissionService);
+ this.groupDto = groupDto;
+ }
+
+ public GroupUuidOrAnyone getGroupUuidOrAnyone() {
+ return GroupUuidOrAnyone.from(groupDto);
+ }
+
+ public Optional<String> getGroupName() {
+ return Optional.ofNullable(groupDto).map(GroupDto::getName);
+ }
+
+ @Override
+ public String getUuidOfGrantee() {
+ return getGroupUuidOrAnyone().getUuid();
+ }
+
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChanger.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChanger.java
new file mode 100644
index 00000000000..23c893ef1ae
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/GroupPermissionChanger.java
@@ -0,0 +1,182 @@
+/*
+ * 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.permission;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.permission.GroupUuidOrAnyone;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.sonar.server.common.permission.Operation.ADD;
+import static org.sonar.server.common.permission.Operation.REMOVE;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+public class GroupPermissionChanger implements GranteeTypeSpecificPermissionUpdater<GroupPermissionChange> {
+
+ private final DbClient dbClient;
+ private final UuidFactory uuidFactory;
+
+ public GroupPermissionChanger(DbClient dbClient, UuidFactory uuidFactory) {
+ this.dbClient = dbClient;
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ public Class<GroupPermissionChange> getHandledClass() {
+ return GroupPermissionChange.class;
+ }
+
+ @Override
+ public Set<String> loadExistingEntityPermissions(DbSession dbSession, String uuidOfGrantee, @Nullable String entityUuid) {
+ if (entityUuid != null) {
+ return new HashSet<>(dbClient.groupPermissionDao().selectEntityPermissionsOfGroup(dbSession, uuidOfGrantee, entityUuid));
+ }
+ return new HashSet<>(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, uuidOfGrantee));
+ }
+
+ @Override
+ public boolean apply(DbSession dbSession, Set<String> existingPermissions, GroupPermissionChange change) {
+ ensureConsistencyWithVisibility(change);
+ if (isImplicitlyAlreadyDone(change)) {
+ return false;
+ }
+ switch (change.getOperation()) {
+ case ADD:
+ if (existingPermissions.contains(change.getPermission())) {
+ return false;
+ }
+ return addPermission(dbSession, change);
+ case REMOVE:
+ if (!existingPermissions.contains(change.getPermission())) {
+ return false;
+ }
+ return removePermission(dbSession, change);
+ default:
+ throw new UnsupportedOperationException("Unsupported permission change: " + change.getOperation());
+ }
+ }
+
+ private static boolean isImplicitlyAlreadyDone(GroupPermissionChange change) {
+ EntityDto project = change.getEntity();
+ if (project != null) {
+ return isImplicitlyAlreadyDone(project, change);
+ }
+ return false;
+ }
+
+ private static boolean isImplicitlyAlreadyDone(EntityDto project, GroupPermissionChange change) {
+ return isAttemptToAddPublicPermissionToPublicComponent(change, project)
+ || isAttemptToRemovePermissionFromAnyoneOnPrivateComponent(change, project);
+ }
+
+ private static boolean isAttemptToAddPublicPermissionToPublicComponent(GroupPermissionChange change, EntityDto project) {
+ return !project.isPrivate()
+ && change.getOperation() == ADD
+ && UserRole.PUBLIC_PERMISSIONS.contains(change.getPermission());
+ }
+
+ private static boolean isAttemptToRemovePermissionFromAnyoneOnPrivateComponent(GroupPermissionChange change, EntityDto project) {
+ return project.isPrivate()
+ && change.getOperation() == REMOVE
+ && change.getGroupUuidOrAnyone().isAnyone();
+ }
+
+ private static void ensureConsistencyWithVisibility(GroupPermissionChange change) {
+ EntityDto project = change.getEntity();
+ if (project != null) {
+ checkRequest(
+ !isAttemptToAddPermissionToAnyoneOnPrivateComponent(change, project),
+ "No permission can be granted to Anyone on a private component");
+ BadRequestException.checkRequest(
+ !isAttemptToRemovePublicPermissionFromPublicComponent(change, project),
+ "Permission %s can't be removed from a public component", change.getPermission());
+ }
+ }
+
+ private static boolean isAttemptToAddPermissionToAnyoneOnPrivateComponent(GroupPermissionChange change, EntityDto project) {
+ return project.isPrivate()
+ && change.getOperation() == ADD
+ && change.getGroupUuidOrAnyone().isAnyone();
+ }
+
+ private static boolean isAttemptToRemovePublicPermissionFromPublicComponent(GroupPermissionChange change, EntityDto project) {
+ return !project.isPrivate()
+ && change.getOperation() == REMOVE
+ && UserRole.PUBLIC_PERMISSIONS.contains(change.getPermission());
+ }
+
+ private boolean addPermission(DbSession dbSession, GroupPermissionChange change) {
+ validateNotAnyoneAndAdminPermission(change.getPermission(), change.getGroupUuidOrAnyone());
+
+ String groupUuid = change.getGroupUuidOrAnyone().getUuid();
+ String groupName = change.getGroupName().orElse(null);
+
+ GroupPermissionDto addedDto = new GroupPermissionDto()
+ .setUuid(uuidFactory.create())
+ .setRole(change.getPermission())
+ .setGroupUuid(groupUuid)
+ .setEntityName(change.getProjectName())
+ .setEntityUuid(change.getProjectUuid())
+ .setGroupName(groupName);
+
+ dbClient.groupPermissionDao().insert(dbSession, addedDto, change.getEntity(), null);
+ return true;
+ }
+
+ private static void validateNotAnyoneAndAdminPermission(String permission, GroupUuidOrAnyone group) {
+ checkRequest(!GlobalPermission.ADMINISTER.getKey().equals(permission) || !group.isAnyone(),
+ format("It is not possible to add the '%s' permission to group 'Anyone'.", permission));
+ }
+
+ private boolean removePermission(DbSession dbSession, GroupPermissionChange change) {
+ checkIfRemainingGlobalAdministrators(dbSession, change);
+ String groupUuid = change.getGroupUuidOrAnyone().getUuid();
+ String groupName = change.getGroupName().orElse(null);
+ dbClient.groupPermissionDao().delete(dbSession,
+ change.getPermission(),
+ groupUuid,
+ groupName,
+ change.getEntity());
+ return true;
+ }
+
+ private void checkIfRemainingGlobalAdministrators(DbSession dbSession, GroupPermissionChange change) {
+ GroupUuidOrAnyone groupUuidOrAnyone = change.getGroupUuidOrAnyone();
+ if (GlobalPermission.ADMINISTER.getKey().equals(change.getPermission()) &&
+ !groupUuidOrAnyone.isAnyone() &&
+ change.getProjectUuid() == null) {
+ String groupUuid = checkNotNull(groupUuidOrAnyone.getUuid());
+ // removing global admin permission from group
+ int remaining = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingGroup(dbSession, GlobalPermission.ADMINISTER.getKey(), groupUuid);
+ checkRequest(remaining > 0, "Last group with permission '%s'. Permission cannot be removed.", GlobalPermission.ADMINISTER.getKey());
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionChange.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionChange.java
new file mode 100644
index 00000000000..30173c3def7
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionChange.java
@@ -0,0 +1,77 @@
+/*
+ * 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.permission;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.server.permission.PermissionService;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+public abstract class PermissionChange {
+
+ private final Operation operation;
+ private final String permission;
+ private final EntityDto entity;
+ protected final PermissionService permissionService;
+
+ protected PermissionChange(Operation operation, String permission, @Nullable EntityDto entity, PermissionService permissionService) {
+ this.operation = requireNonNull(operation);
+ this.permission = requireNonNull(permission);
+ this.entity = entity;
+ this.permissionService = permissionService;
+ if (entity == null) {
+ checkRequest(permissionService.getGlobalPermissions().stream().anyMatch(p -> p.getKey().equals(permission)),
+ "Invalid global permission '%s'. Valid values are %s", permission,
+ permissionService.getGlobalPermissions().stream().map(GlobalPermission::getKey).toList());
+ } else {
+ checkRequest(permissionService.getAllProjectPermissions().contains(permission), "Invalid project permission '%s'. Valid values are %s", permission,
+ permissionService.getAllProjectPermissions());
+ }
+ }
+
+ public Operation getOperation() {
+ return operation;
+ }
+
+ public String getPermission() {
+ return permission;
+ }
+
+ @CheckForNull
+ public EntityDto getEntity() {
+ return entity;
+ }
+
+ @CheckForNull
+ public String getProjectName() {
+ return entity == null ? null : entity.getName();
+ }
+
+ @CheckForNull
+ public String getProjectUuid() {
+ return entity == null ? null : entity.getUuid();
+ }
+
+ public abstract String getUuidOfGrantee();
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionTemplateService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionTemplateService.java
new file mode 100644
index 00000000000..3857d0c3f9c
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionTemplateService.java
@@ -0,0 +1,243 @@
+/*
+ * 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.permission;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.db.permission.UserPermissionDto;
+import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.permission.template.PermissionTemplateGroupDto;
+import org.sonar.db.permission.template.PermissionTemplateUserDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserId;
+import org.sonar.server.es.Indexers;
+import org.sonar.server.exceptions.TemplateMatchingKeyException;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static org.sonar.api.security.DefaultGroups.isAnyone;
+import static org.sonar.api.web.UserRole.PUBLIC_PERMISSIONS;
+import static org.sonar.db.permission.GlobalPermission.SCAN;
+
+@ServerSide
+public class PermissionTemplateService {
+
+ private final DbClient dbClient;
+ private final Indexers indexers;
+ private final UserSession userSession;
+ private final DefaultTemplatesResolver defaultTemplatesResolver;
+ private final UuidFactory uuidFactory;
+
+ public PermissionTemplateService(DbClient dbClient, Indexers indexers, UserSession userSession,
+ DefaultTemplatesResolver defaultTemplatesResolver, UuidFactory uuidFactory) {
+ this.dbClient = dbClient;
+ this.indexers = indexers;
+ this.userSession = userSession;
+ this.defaultTemplatesResolver = defaultTemplatesResolver;
+ this.uuidFactory = uuidFactory;
+ }
+
+ public boolean wouldUserHaveScanPermissionWithDefaultTemplate(DbSession dbSession, @Nullable String userUuid, String projectKey) {
+ if (userSession.hasPermission(SCAN)) {
+ return true;
+ }
+
+ ProjectDto projectDto = new ProjectDto().setKey(projectKey).setQualifier(Qualifiers.PROJECT);
+ PermissionTemplateDto template = findTemplate(dbSession, projectDto);
+ if (template == null) {
+ return false;
+ }
+
+ List<String> potentialPermissions = dbClient.permissionTemplateDao().selectPotentialPermissionsByUserUuidAndTemplateUuid(dbSession, userUuid, template.getUuid());
+ return potentialPermissions.contains(SCAN.getKey());
+ }
+
+ /**
+ * Apply a permission template to a set of projects. Authorization to administrate these projects
+ * is not verified. The projects must exist, so the "project creator" permissions defined in the
+ * template are ignored.
+ */
+ public void applyAndCommit(DbSession dbSession, PermissionTemplateDto template, Collection<EntityDto> entities) {
+ if (entities.isEmpty()) {
+ return;
+ }
+
+ for (EntityDto entity : entities) {
+ dbClient.groupPermissionDao().deleteByEntityUuid(dbSession, entity);
+ dbClient.userPermissionDao().deleteEntityPermissions(dbSession, entity);
+ copyPermissions(dbSession, template, entity, null);
+ }
+ indexers.commitAndIndexEntities(dbSession, entities, Indexers.EntityEvent.PERMISSION_CHANGE);
+ }
+
+ /**
+ * Apply the default permission template to a new project (has no permissions yet).
+ *
+ * @param projectCreatorUserId id of the user creating the project.
+ */
+ public void applyDefaultToNewComponent(DbSession dbSession, EntityDto entityDto, @Nullable String projectCreatorUserId) {
+ PermissionTemplateDto template = findTemplate(dbSession, entityDto);
+ checkArgument(template != null, "Cannot retrieve default permission template");
+ copyPermissions(dbSession, template, entityDto, projectCreatorUserId);
+ }
+
+ public boolean hasDefaultTemplateWithPermissionOnProjectCreator(DbSession dbSession, ProjectDto projectDto) {
+ PermissionTemplateDto template = findTemplate(dbSession, projectDto);
+ return hasProjectCreatorPermission(dbSession, template);
+ }
+
+ private boolean hasProjectCreatorPermission(DbSession dbSession, @Nullable PermissionTemplateDto template) {
+ return template != null && dbClient.permissionTemplateCharacteristicDao().selectByTemplateUuids(dbSession, singletonList(template.getUuid())).stream()
+ .anyMatch(PermissionTemplateCharacteristicDto::getWithProjectCreator);
+ }
+
+ private void copyPermissions(DbSession dbSession, PermissionTemplateDto template, EntityDto entity, @Nullable String projectCreatorUserUuid) {
+ List<PermissionTemplateUserDto> usersPermissions = dbClient.permissionTemplateDao().selectUserPermissionsByTemplateId(dbSession, template.getUuid());
+ Set<String> permissionTemplateUserUuids = usersPermissions.stream().map(PermissionTemplateUserDto::getUserUuid).collect(Collectors.toSet());
+ Map<String, UserId> userIdByUuid = dbClient.userDao().selectByUuids(dbSession, permissionTemplateUserUuids).stream().collect(Collectors.toMap(UserDto::getUuid, u -> u));
+ usersPermissions
+ .stream()
+ .filter(up -> permissionValidForProject(entity.isPrivate(), up.getPermission()))
+ .forEach(up -> {
+ UserPermissionDto dto = new UserPermissionDto(uuidFactory.create(), up.getPermission(), up.getUserUuid(), entity.getUuid());
+ dbClient.userPermissionDao().insert(dbSession, dto, entity, userIdByUuid.get(up.getUserUuid()), template);
+ });
+
+ List<PermissionTemplateGroupDto> groupsPermissions = dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateUuid(dbSession, template.getUuid());
+ groupsPermissions
+ .stream()
+ .filter(gp -> groupNameValidForProject(entity.isPrivate(), gp.getGroupName()))
+ .filter(gp -> permissionValidForProject(entity.isPrivate(), gp.getPermission()))
+ .forEach(gp -> {
+ String groupUuid = isAnyone(gp.getGroupName()) ? null : gp.getGroupUuid();
+ String groupName = groupUuid == null ? null : dbClient.groupDao().selectByUuid(dbSession, groupUuid).getName();
+ GroupPermissionDto dto = new GroupPermissionDto()
+ .setUuid(uuidFactory.create())
+ .setGroupUuid(groupUuid)
+ .setGroupName(groupName)
+ .setRole(gp.getPermission())
+ .setEntityUuid(entity.getUuid())
+ .setEntityName(entity.getName());
+
+ dbClient.groupPermissionDao().insert(dbSession, dto, entity, template);
+ });
+
+ List<PermissionTemplateCharacteristicDto> characteristics = dbClient.permissionTemplateCharacteristicDao().selectByTemplateUuids(dbSession, singletonList(template.getUuid()));
+ if (projectCreatorUserUuid != null) {
+ Set<String> permissionsForCurrentUserAlreadyInDb = usersPermissions.stream()
+ .filter(userPermission -> projectCreatorUserUuid.equals(userPermission.getUserUuid()))
+ .map(PermissionTemplateUserDto::getPermission)
+ .collect(java.util.stream.Collectors.toSet());
+
+ UserDto userDto = dbClient.userDao().selectByUuid(dbSession, projectCreatorUserUuid);
+ characteristics.stream()
+ .filter(PermissionTemplateCharacteristicDto::getWithProjectCreator)
+ .filter(up -> permissionValidForProject(entity.isPrivate(), up.getPermission()))
+ .filter(characteristic -> !permissionsForCurrentUserAlreadyInDb.contains(characteristic.getPermission()))
+ .forEach(c -> {
+ UserPermissionDto dto = new UserPermissionDto(uuidFactory.create(), c.getPermission(), userDto.getUuid(), entity.getUuid());
+ dbClient.userPermissionDao().insert(dbSession, dto, entity, userDto, template);
+ });
+ }
+ }
+
+ private static boolean permissionValidForProject(boolean isPrivateEntity, String permission) {
+ return isPrivateEntity || !PUBLIC_PERMISSIONS.contains(permission);
+ }
+
+ private static boolean groupNameValidForProject(boolean isPrivateEntity, String groupName) {
+ return !isPrivateEntity || !isAnyone(groupName);
+ }
+
+ /**
+ * Return the permission template for the given component. If no template key pattern match then consider default
+ * template for the component qualifier.
+ */
+ @CheckForNull
+ private PermissionTemplateDto findTemplate(DbSession dbSession, EntityDto entityDto) {
+ List<PermissionTemplateDto> allPermissionTemplates = dbClient.permissionTemplateDao().selectAll(dbSession, null);
+ List<PermissionTemplateDto> matchingTemplates = new ArrayList<>();
+ for (PermissionTemplateDto permissionTemplateDto : allPermissionTemplates) {
+ String keyPattern = permissionTemplateDto.getKeyPattern();
+ if (StringUtils.isNotBlank(keyPattern) && entityDto.getKey().matches(keyPattern)) {
+ matchingTemplates.add(permissionTemplateDto);
+ }
+ }
+ checkAtMostOneMatchForComponentKey(entityDto.getKey(), matchingTemplates);
+ if (matchingTemplates.size() == 1) {
+ return matchingTemplates.get(0);
+ }
+
+ String qualifier = entityDto.getQualifier();
+ DefaultTemplatesResolver.ResolvedDefaultTemplates resolvedDefaultTemplates = defaultTemplatesResolver.resolve(dbSession);
+ switch (qualifier) {
+ case Qualifiers.PROJECT:
+ return dbClient.permissionTemplateDao().selectByUuid(dbSession, resolvedDefaultTemplates.getProject());
+ case Qualifiers.VIEW:
+ String portDefaultTemplateUuid = resolvedDefaultTemplates.getPortfolio().orElseThrow(
+ () -> new IllegalStateException("Failed to find default template for portfolios"));
+ return dbClient.permissionTemplateDao().selectByUuid(dbSession, portDefaultTemplateUuid);
+ case Qualifiers.APP:
+ String appDefaultTemplateUuid = resolvedDefaultTemplates.getApplication().orElseThrow(
+ () -> new IllegalStateException("Failed to find default template for applications"));
+ return dbClient.permissionTemplateDao().selectByUuid(dbSession, appDefaultTemplateUuid);
+ default:
+ throw new IllegalArgumentException(format("Qualifier '%s' is not supported", qualifier));
+ }
+ }
+
+ private static void checkAtMostOneMatchForComponentKey(String componentKey, List<PermissionTemplateDto> matchingTemplates) {
+ if (matchingTemplates.size() > 1) {
+ StringBuilder templatesNames = new StringBuilder();
+ for (Iterator<PermissionTemplateDto> it = matchingTemplates.iterator(); it.hasNext(); ) {
+ templatesNames.append("\"").append(it.next().getName()).append("\"");
+ if (it.hasNext()) {
+ templatesNames.append(", ");
+ }
+ }
+ throw new TemplateMatchingKeyException(MessageFormat.format(
+ "The \"{0}\" key matches multiple permission templates: {1}."
+ + " A system administrator must update these templates so that only one of them matches the key.",
+ componentKey,
+ templatesNames.toString()));
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionUpdater.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionUpdater.java
new file mode 100644
index 00000000000..e2beb971b46
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/PermissionUpdater.java
@@ -0,0 +1,78 @@
+/*
+ * 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.permission;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import org.sonar.db.DbSession;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.server.es.Indexers;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toMap;
+import static org.sonar.api.utils.Preconditions.checkState;
+import static org.sonar.server.es.Indexers.EntityEvent.PERMISSION_CHANGE;
+
+public class PermissionUpdater<T extends PermissionChange> {
+
+ private final Indexers indexers;
+
+ private final Map<Class<?>, GranteeTypeSpecificPermissionUpdater<T>> specificPermissionClassToHandler;
+
+ public PermissionUpdater(Indexers indexers, Set<GranteeTypeSpecificPermissionUpdater<T>> permissionChangers) {
+ this.indexers = indexers;
+ specificPermissionClassToHandler = permissionChangers.stream()
+ .collect(toMap(GranteeTypeSpecificPermissionUpdater::getHandledClass, Function.identity()));
+ }
+
+ public void apply(DbSession dbSession, Collection<T> changes) {
+ checkState(changes.stream().map(PermissionChange::getProjectUuid).distinct().count() <= 1,
+ "Only one project per changes is supported");
+
+ List<String> projectOrViewUuids = new ArrayList<>();
+ Map<Optional<String>, List<T>> granteeUuidToPermissionChanges = changes.stream().collect(groupingBy(change -> Optional.ofNullable(change.getUuidOfGrantee())));
+ granteeUuidToPermissionChanges.values().forEach(permissionChanges -> applyForSingleGrantee(dbSession, projectOrViewUuids, permissionChanges));
+
+ indexers.commitAndIndexOnEntityEvent(dbSession, projectOrViewUuids, PERMISSION_CHANGE);
+ }
+
+ private void applyForSingleGrantee(DbSession dbSession, List<String> projectOrViewUuids, List<T> permissionChanges) {
+ T anyPermissionChange = permissionChanges.iterator().next();
+ EntityDto entity = anyPermissionChange.getEntity();
+ String entityUuid = Optional.ofNullable(entity).map(EntityDto::getUuid).orElse(null);
+ GranteeTypeSpecificPermissionUpdater<T> granteeTypeSpecificPermissionUpdater = getSpecificProjectUpdater(anyPermissionChange);
+ Set<String> existingPermissions = granteeTypeSpecificPermissionUpdater.loadExistingEntityPermissions(dbSession, anyPermissionChange.getUuidOfGrantee(), entityUuid);
+ for (T permissionChange : permissionChanges) {
+ if (granteeTypeSpecificPermissionUpdater.apply(dbSession, existingPermissions, permissionChange) && permissionChange.getProjectUuid() != null) {
+ projectOrViewUuids.add(permissionChange.getProjectUuid());
+ }
+ }
+ }
+
+ private GranteeTypeSpecificPermissionUpdater<T> getSpecificProjectUpdater(T anyPermissionChange) {
+ return specificPermissionClassToHandler.get(anyPermissionChange.getClass());
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChange.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChange.java
new file mode 100644
index 00000000000..acb5c260065
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChange.java
@@ -0,0 +1,48 @@
+/*
+ * 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.permission;
+
+import javax.annotation.Nullable;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.user.UserId;
+import org.sonar.server.common.permission.Operation;
+import org.sonar.server.permission.PermissionService;
+
+import static java.util.Objects.requireNonNull;
+
+public class UserPermissionChange extends PermissionChange {
+
+ private final UserId userId;
+
+ public UserPermissionChange(Operation operation, String permission, @Nullable EntityDto entity, UserId userId,
+ PermissionService permissionService) {
+ super(operation, permission, entity, permissionService);
+ this.userId = requireNonNull(userId);
+ }
+
+ public UserId getUserId() {
+ return userId;
+ }
+
+ @Override
+ public String getUuidOfGrantee() {
+ return userId.getUuid();
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChanger.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChanger.java
new file mode 100644
index 00000000000..3874f7b1f48
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/permission/UserPermissionChanger.java
@@ -0,0 +1,142 @@
+/*
+ * 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.permission;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.jetbrains.annotations.Nullable;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.permission.UserPermissionDto;
+
+import static org.sonar.server.common.permission.Operation.ADD;
+import static org.sonar.server.common.permission.Operation.REMOVE;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+/**
+ * Adds and removes user permissions. Both global and project scopes are supported.
+ */
+public class UserPermissionChanger implements GranteeTypeSpecificPermissionUpdater<UserPermissionChange> {
+
+ private final DbClient dbClient;
+ private final UuidFactory uuidFactory;
+
+ public UserPermissionChanger(DbClient dbClient, UuidFactory uuidFactory) {
+ this.dbClient = dbClient;
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ public Class<UserPermissionChange> getHandledClass() {
+ return UserPermissionChange.class;
+ }
+
+ @Override
+ public Set<String> loadExistingEntityPermissions(DbSession dbSession, String uuidOfGrantee, @Nullable String entityUuid) {
+ if (entityUuid != null) {
+ return new HashSet<>(dbClient.userPermissionDao().selectEntityPermissionsOfUser(dbSession, uuidOfGrantee, entityUuid));
+ }
+ return new HashSet<>(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, uuidOfGrantee));
+ }
+
+ @Override
+ public boolean apply(DbSession dbSession, Set<String> existingPermissions, UserPermissionChange change) {
+ ensureConsistencyWithVisibility(change);
+ if (isImplicitlyAlreadyDone(change)) {
+ return false;
+ }
+ switch (change.getOperation()) {
+ case ADD:
+ return addPermission(dbSession, existingPermissions, change);
+ case REMOVE:
+ return removePermission(dbSession, existingPermissions, change);
+ default:
+ throw new UnsupportedOperationException("Unsupported permission change: " + change.getOperation());
+ }
+ }
+
+ private static boolean isImplicitlyAlreadyDone(UserPermissionChange change) {
+ EntityDto project = change.getEntity();
+ if (project != null) {
+ return isImplicitlyAlreadyDone(project, change);
+ }
+ return false;
+ }
+
+ private static boolean isImplicitlyAlreadyDone(EntityDto project, UserPermissionChange change) {
+ return isAttemptToAddPublicPermissionToPublicComponent(change, project);
+ }
+
+ private static boolean isAttemptToAddPublicPermissionToPublicComponent(UserPermissionChange change, EntityDto project) {
+ return !project.isPrivate()
+ && change.getOperation() == ADD
+ && UserRole.PUBLIC_PERMISSIONS.contains(change.getPermission());
+ }
+
+ private static void ensureConsistencyWithVisibility(UserPermissionChange change) {
+ EntityDto project = change.getEntity();
+ if (project != null) {
+ checkRequest(!isAttemptToRemovePublicPermissionFromPublicComponent(change, project),
+ "Permission %s can't be removed from a public component", change.getPermission());
+ }
+ }
+
+ private static boolean isAttemptToRemovePublicPermissionFromPublicComponent(UserPermissionChange change, EntityDto entity) {
+ return !entity.isPrivate()
+ && change.getOperation() == REMOVE
+ && UserRole.PUBLIC_PERMISSIONS.contains(change.getPermission());
+ }
+
+ private boolean addPermission(DbSession dbSession, Set<String> existingPermissions, UserPermissionChange change) {
+ if (existingPermissions.contains(change.getPermission())) {
+ return false;
+ }
+ UserPermissionDto dto = new UserPermissionDto(uuidFactory.create(), change.getPermission(), change.getUserId().getUuid(),
+ change.getProjectUuid());
+ dbClient.userPermissionDao().insert(dbSession, dto, change.getEntity(), change.getUserId(), null);
+ return true;
+ }
+
+ private boolean removePermission(DbSession dbSession, Set<String> existingPermissions, UserPermissionChange change) {
+ if (!existingPermissions.contains(change.getPermission())) {
+ return false;
+ }
+ checkOtherAdminsExist(dbSession, change);
+ EntityDto entity = change.getEntity();
+ if (entity != null) {
+ dbClient.userPermissionDao().deleteEntityPermission(dbSession, change.getUserId(), change.getPermission(), entity);
+ } else {
+ dbClient.userPermissionDao().deleteGlobalPermission(dbSession, change.getUserId(), change.getPermission());
+ }
+ return true;
+ }
+
+ private void checkOtherAdminsExist(DbSession dbSession, UserPermissionChange change) {
+ if (GlobalPermission.ADMINISTER.getKey().equals(change.getPermission()) && change.getProjectUuid() == null) {
+ int remaining = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingUserPermission(dbSession, change.getPermission(), change.getUserId().getUuid());
+ checkRequest(remaining > 0, "Last user with permission '%s'. Permission cannot be removed.", GlobalPermission.ADMINISTER.getKey());
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ProjectCreator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ProjectCreator.java
new file mode 100644
index 00000000000..22cf07233f4
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/ProjectCreator.java
@@ -0,0 +1,71 @@
+/*
+ * 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.project;
+
+import javax.annotation.Nullable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.server.common.component.ComponentCreationParameters;
+import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.component.NewComponent;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.project.ProjectDefaultVisibility;
+import org.sonar.server.user.UserSession;
+
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+
+@ServerSide
+public class ProjectCreator {
+
+ private final UserSession userSession;
+ private final ProjectDefaultVisibility projectDefaultVisibility;
+ private final ComponentUpdater componentUpdater;
+
+ public ProjectCreator(UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater) {
+ this.userSession = userSession;
+ this.projectDefaultVisibility = projectDefaultVisibility;
+ this.componentUpdater = componentUpdater;
+ }
+
+ public ComponentCreationData createProject(DbSession dbSession, String projectKey, String projectName, @Nullable String mainBranchName, CreationMethod creationMethod,
+ @Nullable Boolean isPrivate, boolean isManaged) {
+ boolean visibility = isPrivate != null ? isPrivate : projectDefaultVisibility.get(dbSession).isPrivate();
+ NewComponent projectComponent = NewComponent.newComponentBuilder()
+ .setKey(projectKey)
+ .setName(projectName)
+ .setPrivate(visibility)
+ .setQualifier(PROJECT)
+ .build();
+ ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
+ .newComponent(projectComponent)
+ .userLogin(userSession.getLogin())
+ .userUuid(userSession.getUuid())
+ .mainBranchName(mainBranchName)
+ .isManaged(isManaged)
+ .creationMethod(creationMethod)
+ .build();
+ return componentUpdater.createWithoutCommit(dbSession, componentCreationParameters);
+ }
+
+ public ComponentCreationData createProject(DbSession dbSession, String projectKey, String projectName, @Nullable String mainBranchName, CreationMethod creationMethod) {
+ return createProject(dbSession, projectKey, projectName, mainBranchName, creationMethod, projectDefaultVisibility.get(dbSession).isPrivate(), false);
+ }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/package-info.java
new file mode 100644
index 00000000000..174ee7fd7cb
--- /dev/null
+++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/project/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.project;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almintegration/ProjectKeyGeneratorTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almintegration/ProjectKeyGeneratorTest.java
new file mode 100644
index 00000000000..3751d757bda
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almintegration/ProjectKeyGeneratorTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.almintegration;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.core.util.UuidFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.common.almintegration.ProjectKeyGenerator.MAX_PROJECT_KEY_SIZE;
+import static org.sonar.server.common.almintegration.ProjectKeyGenerator.PROJECT_KEY_SEPARATOR;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectKeyGeneratorTest {
+
+ private static final int MAX_UUID_SIZE = 40;
+ private static final String UUID_STRING = RandomStringUtils.randomAlphanumeric(MAX_UUID_SIZE);
+
+ @Mock
+ private UuidFactory uuidFactory;
+
+ @InjectMocks
+ private ProjectKeyGenerator projectKeyGenerator;
+
+ @Before
+ public void setUp() {
+ when(uuidFactory.create()).thenReturn(UUID_STRING);
+ }
+
+ @Test
+ public void generateUniqueProjectKey_shortProjectName_shouldAppendUuid() {
+ String fullProjectName = RandomStringUtils.randomAlphanumeric(10);
+
+ assertThat(projectKeyGenerator.generateUniqueProjectKey(fullProjectName))
+ .isEqualTo(generateExpectedKeyName(fullProjectName));
+ }
+
+ @Test
+ public void generateUniqueProjectKey_projectNameEqualsToMaximumSize_shouldTruncateProjectNameAndPreserveUUID() {
+ String fullProjectName = RandomStringUtils.randomAlphanumeric(MAX_PROJECT_KEY_SIZE);
+
+ String projectKey = projectKeyGenerator.generateUniqueProjectKey(fullProjectName);
+ assertThat(projectKey)
+ .hasSize(MAX_PROJECT_KEY_SIZE)
+ .isEqualTo(generateExpectedKeyName(fullProjectName.substring(fullProjectName.length() + UUID_STRING.length() + 1 - MAX_PROJECT_KEY_SIZE)));
+ }
+
+ @Test
+ public void generateUniqueProjectKey_projectNameBiggerThanMaximumSize_shouldTruncateProjectNameAndPreserveUUID() {
+ String fullProjectName = RandomStringUtils.randomAlphanumeric(MAX_PROJECT_KEY_SIZE + 50);
+
+ String projectKey = projectKeyGenerator.generateUniqueProjectKey(fullProjectName);
+ assertThat(projectKey)
+ .hasSize(MAX_PROJECT_KEY_SIZE)
+ .isEqualTo(generateExpectedKeyName(fullProjectName.substring(fullProjectName.length() + UUID_STRING.length() + 1 - MAX_PROJECT_KEY_SIZE)));
+ }
+
+ @Test
+ public void generateUniqueProjectKey_projectNameContainsSlashes_shouldBeEscaped() {
+ String fullProjectName = "a/b/c";
+
+ assertThat(projectKeyGenerator.generateUniqueProjectKey(fullProjectName))
+ .isEqualTo(generateExpectedKeyName(fullProjectName.replace("/", "_")));
+ }
+
+ private String generateExpectedKeyName(String truncatedProjectName) {
+ return truncatedProjectName + PROJECT_KEY_SEPARATOR + UUID_STRING;
+ }
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactoryTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactoryTest.java
new file mode 100644
index 00000000000..b8815607956
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/DelegatingDevOpsProjectCreatorFactoryTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.almsettings;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.Test;
+import org.sonar.db.DbSession;
+
+import static java.util.Collections.emptySet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DelegatingDevOpsProjectCreatorFactoryTest {
+
+ private static final DbSession DB_SESSION = mock();
+ private static final Map<String, String> CHARACTERISTICS = Map.of("toto", "tata");
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenNoDelegates_shouldReturnEmptyOptional() {
+ DelegatingDevOpsProjectCreatorFactory noDelegates = new DelegatingDevOpsProjectCreatorFactory(emptySet());
+ Optional<DevOpsProjectCreator> devOpsProjectCreator = noDelegates.getDevOpsProjectCreator(DB_SESSION, CHARACTERISTICS);
+ assertThat(devOpsProjectCreator).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenNoDelegatesReturningACreator_shouldReturnEmptyOptional() {
+ DelegatingDevOpsProjectCreatorFactory delegates = new DelegatingDevOpsProjectCreatorFactory(Set.of(mock(), mock()));
+ Optional<DevOpsProjectCreator> devOpsProjectCreator = delegates.getDevOpsProjectCreator(DB_SESSION, CHARACTERISTICS);
+
+ assertThat(devOpsProjectCreator).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenOneDelegatesReturningACreator_shouldDelegate() {
+ DevOpsProjectCreatorFactory successfulDelegate = mock();
+ DevOpsProjectCreator devOpsProjectCreator = mock();
+ when(successfulDelegate.getDevOpsProjectCreator(DB_SESSION, CHARACTERISTICS)).thenReturn(Optional.of(devOpsProjectCreator));
+ DelegatingDevOpsProjectCreatorFactory delegates = new DelegatingDevOpsProjectCreatorFactory(Set.of(mock(), successfulDelegate));
+
+ assertThat(delegates.getDevOpsProjectCreator(DB_SESSION, CHARACTERISTICS)).contains(devOpsProjectCreator);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactoryTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactoryTest.java
new file mode 100644
index 00000000000..ac805ea74ab
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorFactoryTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.almsettings.github;
+
+import java.util.List;
+import java.util.Map;
+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.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.alm.client.github.GithubPermissionConverter;
+import org.sonar.auth.github.AppInstallationToken;
+import org.sonar.auth.github.GitHubSettings;
+import org.sonar.auth.github.client.GithubApplicationClient;
+import org.sonar.auth.github.security.AccessToken;
+import org.sonar.auth.github.security.UserAccessToken;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.almsettings.github.GithubProjectCreationParameters;
+import org.sonar.server.common.almsettings.github.GithubProjectCreator;
+import org.sonar.server.common.almsettings.github.GithubProjectCreatorFactory;
+import org.sonar.server.exceptions.BadConfigurationException;
+import org.sonar.server.management.ManagedProjectService;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.project.ProjectDefaultVisibility;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_PROJECT_IDENTIFIER;
+import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_URL;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GithubProjectCreatorFactoryTest {
+ private static final String PROJECT_NAME = "projectName";
+ 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 GITHUB_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME);
+ private static final Map<String, String> VALID_GITHUB_PROJECT_COORDINATES = Map.of(
+ DEVOPS_PLATFORM_URL, GITHUB_PROJECT_DESCRIPTOR.url(),
+ DEVOPS_PLATFORM_PROJECT_IDENTIFIER, GITHUB_PROJECT_DESCRIPTOR.projectIdentifier());
+ private static final long APP_INSTALLATION_ID = 534534534543L;
+ private static final String USER_ACCESS_TOKEN = "userPat";
+
+ @Mock
+ private DbSession dbSession;
+ @Mock
+ private GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+ @Mock
+ private GithubApplicationClient githubApplicationClient;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private DbClient dbClient;
+ @Mock
+ private UserSession userSession;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private ProjectDefaultVisibility projectDefaultVisibility;
+ @Mock
+ private ProjectKeyGenerator projectKeyGenerator;
+ @Mock
+ private GitHubSettings gitHubSettings;
+ @Mock
+ private GithubPermissionConverter githubPermissionConverter;
+ @Mock
+ private AppInstallationToken appInstallationToken;
+ @Mock
+ private AppInstallationToken authAppInstallationToken;
+ @Mock
+ private PermissionService permissionService;
+ @Mock
+ private PermissionUpdater<UserPermissionChange> permissionUpdater;
+ @Mock
+ private ManagedProjectService managedProjectService;
+ @Mock
+ private ProjectCreator projectCreator;
+
+ @InjectMocks
+ private GithubProjectCreatorFactory githubProjectCreatorFactory;
+
+ @Test
+ public void getDevOpsProjectCreator_whenNoCharacteristics_shouldReturnEmpty() {
+ Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, Map.of());
+
+ assertThat(devOpsProjectCreator).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenValidCharacteristicsButNoAlmSettingDao_shouldReturnEmpty() {
+ Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES);
+ assertThat(devOpsProjectCreator).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenValidCharacteristicsButInvalidAlmSettingDto_shouldThrow() {
+ AlmSettingDto almSettingDto = mockAlmSettingDto(true);
+ IllegalArgumentException error = new IllegalArgumentException("error happened");
+ when(githubGlobalSettingsValidator.validate(almSettingDto)).thenThrow(error);
+
+ assertThatIllegalArgumentException().isThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES))
+ .isSameAs(error);
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenAppHasNoAccessToRepo_shouldReturnEmpty() {
+ mockAlmSettingDto(true);
+ when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.empty());
+
+ Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES);
+ assertThat(devOpsProjectCreator).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenNotPossibleToGenerateToken_shouldThrow() {
+ AlmSettingDto almSettingDto = mockAlmSettingDto(true);
+ when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(APP_INSTALLATION_ID));
+ when(githubGlobalSettingsValidator.validate(almSettingDto)).thenReturn(mock());
+ when(githubApplicationClient.createAppInstallationToken(any(), eq(APP_INSTALLATION_ID))).thenReturn(Optional.empty());
+
+ assertThatIllegalStateException().isThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES))
+ .withMessage("Error while generating token for GitHub Api Url null (installation id: 534534534543)");
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenOneValidAlmSetting_shouldInstantiateDevOpsProjectCreator() {
+ AlmSettingDto almSettingDto = mockAlmSettingDto(true);
+ mockSuccessfulGithubInteraction();
+
+ DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
+
+ GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(almSettingDto, false, appInstallationToken);
+ assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenOneValidAlmSettingAndPublicByDefaultAndAutoProvisioningEnabled_shouldInstantiateDevOpsProjectCreatorAndDefineAnAuthAppToken() {
+ AlmSettingDto almSettingDto = mockAlmSettingDto(true);
+ mockSuccessfulGithubInteraction();
+
+ when(projectDefaultVisibility.get(any()).isPrivate()).thenReturn(true);
+ mockValidGitHubSettings();
+
+ long authAppInstallationId = 32;
+ when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(authAppInstallationId));
+ when(githubApplicationClient.createAppInstallationToken(any(), eq(authAppInstallationId))).thenReturn(Optional.of(authAppInstallationToken));
+
+ DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
+
+ GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(almSettingDto, true, appInstallationToken);
+ assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
+ }
+
+ @Test
+ public void getDevOpsProjectCreator_whenOneMatchingAndOneNotMatchingAlmSetting_shouldInstantiateDevOpsProjectCreator() {
+ AlmSettingDto matchingAlmSettingDto = mockAlmSettingDto(true);
+ AlmSettingDto notMatchingAlmSettingDto = mockAlmSettingDto(false);
+ when(dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(notMatchingAlmSettingDto, matchingAlmSettingDto));
+
+ mockSuccessfulGithubInteraction();
+
+ DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
+
+ GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(matchingAlmSettingDto, false, appInstallationToken);
+ assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
+ }
+
+ @Test
+ public void getDevOpsProjectCreatorFromImport_shouldInstantiateDevOpsProjectCreator() {
+ AlmSettingDto mockAlmSettingDto = mockAlmSettingDto(true);
+ mockAlmPatDto(mockAlmSettingDto);
+
+ mockSuccessfulGithubInteraction();
+
+ DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR).orElseThrow();
+
+ GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(mockAlmSettingDto, false, new UserAccessToken(USER_ACCESS_TOKEN));
+ assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
+ }
+
+ @Test
+ public void getDevOpsProjectCreatorFromImport_whenGitHubConfigDoesNotAllowAccessToRepo_shouldThrow() {
+ AlmSettingDto mockAlmSettingDto = mockAlmSettingDto(false);
+ mockAlmPatDto(mockAlmSettingDto);
+
+ mockValidGitHubSettings();
+
+ when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.empty());
+
+ assertThatThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR))
+ .isInstanceOf(BadConfigurationException.class)
+ .hasMessage(format("GitHub auto-provisioning is activated. However the repo %s is not in the scope of the authentication application. "
+ + "The permissions can't be checked, and the project can not be created.",
+ GITHUB_REPO_FULL_NAME));
+ }
+
+ private void mockValidGitHubSettings() {
+ when(gitHubSettings.appId()).thenReturn("4324");
+ when(gitHubSettings.privateKey()).thenReturn("privateKey");
+ when(gitHubSettings.apiURL()).thenReturn(GITHUB_API_URL);
+ when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
+ }
+
+ private void mockSuccessfulGithubInteraction() {
+ when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(APP_INSTALLATION_ID));
+ when(githubApplicationClient.createAppInstallationToken(any(), eq(APP_INSTALLATION_ID))).thenReturn(Optional.of(appInstallationToken));
+ }
+
+ private GithubProjectCreator getExpectedGithubProjectCreator(AlmSettingDto almSettingDto, boolean isInstanceManaged, AccessToken accessToken) {
+ DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, almSettingDto.getUrl(), GITHUB_REPO_FULL_NAME);
+ AppInstallationToken authAppInstallToken = isInstanceManaged ? authAppInstallationToken : null;
+ GithubProjectCreationParameters githubProjectCreationParameters = new GithubProjectCreationParameters(devOpsProjectDescriptor, almSettingDto, userSession, accessToken,
+ authAppInstallToken);
+ return new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator, permissionUpdater, permissionService,
+ managedProjectService, projectCreator, githubProjectCreationParameters, gitHubSettings);
+ }
+
+ private AlmSettingDto mockAlmSettingDto(boolean repoAccess) {
+ AlmSettingDto almSettingDto = mock();
+ when(almSettingDto.getUrl()).thenReturn(repoAccess ? GITHUB_PROJECT_DESCRIPTOR.url() : "anotherUrl");
+ when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB);
+
+ when(dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(almSettingDto));
+ return almSettingDto;
+ }
+
+ private void mockAlmPatDto(AlmSettingDto almSettingDto) {
+ when(userSession.getUuid()).thenReturn("userUuid");
+ when(dbClient.almPatDao().selectByUserAndAlmSetting(any(), eq("userUuid"), eq(almSettingDto)))
+ .thenReturn(Optional.of(new AlmPatDto().setPersonalAccessToken(USER_ACCESS_TOKEN)));
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorTest.java
new file mode 100644
index 00000000000..dcff559a270
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/github/GithubProjectCreatorTest.java
@@ -0,0 +1,478 @@
+/*
+ * 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.almsettings.github;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.alm.client.github.GithubPermissionConverter;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.web.UserRole;
+import org.sonar.auth.github.AppInstallationToken;
+import org.sonar.auth.github.GitHubSettings;
+import org.sonar.auth.github.GsonRepositoryCollaborator;
+import org.sonar.auth.github.GsonRepositoryPermissions;
+import org.sonar.auth.github.GsonRepositoryTeam;
+import org.sonar.auth.github.client.GithubApplicationClient;
+import org.sonar.auth.github.security.AccessToken;
+import org.sonar.db.DbClient;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDao;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.component.BranchDto;
+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.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.component.ComponentCreationParameters;
+import org.sonar.server.common.component.ComponentUpdater;
+import org.sonar.server.common.component.NewComponent;
+import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.permission.UserPermissionChange;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.management.ManagedProjectService;
+import org.sonar.server.permission.PermissionService;
+import org.sonar.server.permission.PermissionServiceImpl;
+import org.sonar.server.project.ProjectDefaultVisibility;
+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;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.project.CreationMethod.ALM_IMPORT_API;
+import static org.sonar.db.project.CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG;
+
+@ExtendWith(MockitoExtension.class)
+class GithubProjectCreatorTest {
+
+ private static final String ORGANIZATION_NAME = "orga2";
+ private static final String REPOSITORY_NAME = "repo1";
+
+ private static final String MAIN_BRANCH_NAME = "defaultBranch";
+ private static final DevOpsProjectDescriptor DEVOPS_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, "http://api.com", ORGANIZATION_NAME + "/" + REPOSITORY_NAME);
+ private static final String ALM_SETTING_KEY = "github_config_1";
+ private static final String USER_LOGIN = "userLogin";
+ private static final String USER_UUID = "userUuid";
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private DbClient dbClient;
+ @Mock
+ private GithubApplicationClient githubApplicationClient;
+ @Mock
+ private GithubPermissionConverter githubPermissionConverter;
+ @Mock
+ private ProjectKeyGenerator projectKeyGenerator;
+ @Mock
+ private ComponentUpdater componentUpdater;
+ @Mock
+ private GithubProjectCreationParameters githubProjectCreationParameters;
+ @Mock
+ private AccessToken devOpsAppInstallationToken;
+ @Mock
+ private AppInstallationToken authAppInstallationToken;
+ @Mock
+ private UserSession userSession;
+ @Mock
+ private AlmSettingDto almSettingDto;
+ private final PermissionService permissionService = new PermissionServiceImpl(mock());
+ @Mock
+ private PermissionUpdater<UserPermissionChange> permissionUpdater;
+ @Mock
+ private ManagedProjectService managedProjectService;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private ProjectDefaultVisibility projectDefaultVisibility;
+ private final GitHubSettings gitHubSettings = mock();
+
+ private GithubProjectCreator githubProjectCreator;
+
+ @Captor
+ ArgumentCaptor<ComponentCreationParameters> componentCreationParametersCaptor;
+ @Captor
+ ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingDtoCaptor;
+
+ @BeforeEach
+ void setup() {
+ lenient().when(userSession.getLogin()).thenReturn(USER_LOGIN);
+ lenient().when(userSession.getUuid()).thenReturn(USER_UUID);
+
+ lenient().when(almSettingDto.getUrl()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.url());
+ lenient().when(almSettingDto.getKey()).thenReturn(ALM_SETTING_KEY);
+
+ when(githubProjectCreationParameters.devOpsProjectDescriptor()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR);
+ when(githubProjectCreationParameters.userSession()).thenReturn(userSession);
+ when(githubProjectCreationParameters.devOpsAppInstallationToken()).thenReturn(devOpsAppInstallationToken);
+ when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(authAppInstallationToken);
+ when(githubProjectCreationParameters.almSettingDto()).thenReturn(almSettingDto);
+
+ ProjectCreator projectCreator = new ProjectCreator(userSession, projectDefaultVisibility, componentUpdater);
+ githubProjectCreator = new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator,
+ permissionUpdater, permissionService, managedProjectService, projectCreator, githubProjectCreationParameters, gitHubSettings);
+
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenNoAuthToken_throws() {
+ when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(null);
+
+ assertThatIllegalStateException().isThrownBy(() -> githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform())
+ .withMessage("An auth app token is required in case repository permissions checking is necessary.");
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsNotAGitHubUser_returnsFalse() {
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccessButNoScanPermissions_returnsFalse() {
+ GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
+ mockGithubCollaboratorsFromApi(collaborator1);
+ bindSessionToCollaborator(collaborator1);
+
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccess_returnsTrue() {
+ GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
+ GsonRepositoryCollaborator collaborator2 = mockCollaborator("collaborator2", 2, "role2", "read", "scan");
+ mockGithubCollaboratorsFromApi(collaborator1, collaborator2);
+ bindSessionToCollaborator(collaborator2);
+
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButNoScanPermissions_returnsFalse() {
+ GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.ADMIN);
+ mockTeamsFromApi(team2);
+ bindGroupsToUser(team2.name());
+
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeam_returnsTrue() {
+ GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
+ GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
+ mockTeamsFromApi(team1, team2);
+ bindGroupsToUser(team1.name(), team2.name());
+
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButUserNotInTeam_returnsFalse() {
+ GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
+ GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
+ mockTeamsFromApi(team1, team2);
+ bindGroupsToUser(team1.name());
+
+ assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
+ }
+
+ 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 GsonRepositoryCollaborator mockCollaborator(String collaboratorLogin, int id, String role1, String... sqPermissions) {
+ GsonRepositoryCollaborator collaborator = new GsonRepositoryCollaborator(collaboratorLogin, id, role1,
+ new GsonRepositoryPermissions(false, false, false, false, false));
+ mockPermissionsConversion(collaborator, sqPermissions);
+ return collaborator;
+ }
+
+ private void mockGithubCollaboratorsFromApi(GsonRepositoryCollaborator... repositoryCollaborators) {
+ Set<GsonRepositoryCollaborator> collaborators = Arrays.stream(repositoryCollaborators).collect(toSet());
+ when(githubApplicationClient.getRepositoryCollaborators(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME)).thenReturn(
+ collaborators);
+ }
+
+ private GsonRepositoryTeam mockGithubTeam(String name, int id, String role, String... sqPermissions) {
+ GsonRepositoryTeam gsonRepositoryTeam = new GsonRepositoryTeam(name, id, name + "slug", role, new GsonRepositoryPermissions(false, false, false, false, false));
+ mockPermissionsConversion(gsonRepositoryTeam, sqPermissions);
+ return gsonRepositoryTeam;
+ }
+
+ private void mockTeamsFromApi(GsonRepositoryTeam... repositoryTeams) {
+ when(githubApplicationClient.getRepositoryTeams(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME))
+ .thenReturn(Arrays.stream(repositoryTeams).collect(toSet()));
+ }
+
+ private void mockPermissionsConversion(GsonRepositoryCollaborator collaborator, String... sqPermissions) {
+ Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
+ lenient().when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, collaborator.roleName(), collaborator.permissions()))
+ .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
+ }
+
+ private void mockPermissionsConversion(GsonRepositoryTeam team, String... sqPermissions) {
+ Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
+ lenient().when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, team.permission(), team.permissions()))
+ .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
+ }
+
+ private Set<GithubPermissionsMappingDto> mockPermissionsMappingsDtos() {
+ Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = Set.of(mock(GithubPermissionsMappingDto.class));
+ when(dbClient.githubPermissionsMappingDao().findAll(any())).thenReturn(githubPermissionsMappingDtos);
+ return githubPermissionsMappingDtos;
+ }
+
+ private void bindGroupsToUser(String... groupNames) {
+ Set<GroupDto> groupDtos = Arrays.stream(groupNames)
+ .map(groupName -> new GroupDto().setName(ORGANIZATION_NAME + "/" + groupName).setUuid("uuid_" + groupName))
+ .collect(toSet());
+ when(userSession.getGroups()).thenReturn(groupDtos);
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() {
+ assertThatIllegalStateException().isThrownBy(
+ () -> githubProjectCreator.createProjectAndBindToDevOpsPlatform(mock(), SCANNER_API_DEVOPS_AUTO_CONFIG, false, null, null))
+ .withMessage("Impossible to find the repository 'orga2/repo1' on GitHub, using the devops config " + ALM_SETTING_KEY);
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatformFromScanner_whenRepoFoundOnGitHub_successfullyCreatesProject() {
+ // given
+ mockGitHubRepository();
+
+ ComponentCreationData componentCreationData = mockProjectCreation("generated_orga2/repo1");
+ ProjectAlmSettingDao projectAlmSettingDao = mock();
+ when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
+ when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+
+ // when
+ ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true),
+ SCANNER_API_DEVOPS_AUTO_CONFIG, false, null, null);
+
+ // then
+ assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
+
+ ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
+ assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, "generated_orga2/repo1", SCANNER_API_DEVOPS_AUTO_CONFIG);
+ assertThat(componentCreationParameters.isManaged()).isFalse();
+ assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
+
+ verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq("generated_orga2/repo1"));
+ ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
+ assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatformFromScanner_whenRepoFoundOnGitHubAndVisibilitySynchronizationEnabled_successfullyCreatesProjectAndSetsVisibility() {
+ // given
+ mockPublicGithubRepository();
+
+ ComponentCreationData componentCreationData = mockProjectCreation("generated_orga2/repo1");
+ ProjectAlmSettingDao projectAlmSettingDao = mock();
+ when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
+ when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
+ when(gitHubSettings.isProjectVisibilitySynchronizationActivated()).thenReturn(true);
+
+ // when
+ ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true),
+ SCANNER_API_DEVOPS_AUTO_CONFIG, false, null, null);
+
+ // then
+ assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
+
+ ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
+ assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatformFromScanner_whenRepoFoundOnGitHubAndVisibilitySynchronizationDisabled_successfullyCreatesProjectAndMakesProjectPrivate() {
+ // given
+ mockGitHubRepository();
+
+ ComponentCreationData componentCreationData = mockProjectCreation("generated_orga2/repo1");
+ ProjectAlmSettingDao projectAlmSettingDao = mock();
+ when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
+ when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
+ when(gitHubSettings.isProjectVisibilitySynchronizationActivated()).thenReturn(false);
+
+ // when
+ ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true),
+ SCANNER_API_DEVOPS_AUTO_CONFIG, false, null, null);
+
+ // then
+ assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
+
+ ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
+ assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHub_successfullyCreatesProject() {
+ // given
+ String projectKey = "customProjectKey";
+ mockGitHubRepository();
+
+ ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
+ ProjectAlmSettingDao projectAlmSettingDao = mock();
+ when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
+ when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+
+ // when
+ ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, false, projectKey,
+ null);
+
+ // then
+ assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
+
+ ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
+ assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
+ assertThat(componentCreationParameters.isManaged()).isFalse();
+ assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
+
+ verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
+ ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
+ assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
+ }
+
+ @Captor
+ private ArgumentCaptor<Collection<UserPermissionChange>> permissionChangesCaptor;
+
+ @Test
+ void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHubAutoProvisioningOnAndRepoPrivate_successfullyCreatesProject() {
+ // given
+ String projectKey = "customProjectKey";
+ mockGitHubRepository();
+
+ ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
+ ProjectAlmSettingDao projectAlmSettingDao = mock();
+ when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
+ when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
+
+ // when
+ ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, false, projectKey,
+ null);
+
+ // then
+ assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
+
+ ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
+ assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
+ assertThat(componentCreationParameters.isManaged()).isTrue();
+ assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
+
+ verifyScanPermissionWasAddedToUser(actualComponentCreationData);
+ verifyProjectSyncTaskWasCreated(actualComponentCreationData);
+
+ verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
+ ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
+ assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
+ }
+
+ private void verifyProjectSyncTaskWasCreated(ComponentCreationData componentCreationData) {
+ String projectUuid = requireNonNull(componentCreationData.projectDto()).getUuid();
+ String mainBranchUuid = requireNonNull(componentCreationData.mainBranchDto()).getUuid();
+ verify(managedProjectService).queuePermissionSyncTask(USER_UUID, mainBranchUuid, projectUuid);
+ }
+
+ private void verifyScanPermissionWasAddedToUser(ComponentCreationData actualComponentCreationData) {
+ verify(permissionUpdater).apply(any(), permissionChangesCaptor.capture());
+ UserPermissionChange permissionChange = permissionChangesCaptor.getValue().iterator().next();
+ assertThat(permissionChange.getUserId().getUuid()).isEqualTo(userSession.getUuid());
+ assertThat(permissionChange.getUserId().getLogin()).isEqualTo(userSession.getLogin());
+ assertThat(permissionChange.getPermission()).isEqualTo(UserRole.SCAN);
+ assertThat(permissionChange.getProjectUuid()).isEqualTo(actualComponentCreationData.projectDto().getUuid());
+ }
+
+ private void mockPublicGithubRepository() {
+ GithubApplicationClient.Repository repository = mockGitHubRepository();
+ when(repository.isPrivate()).thenReturn(false);
+ }
+
+ private GithubApplicationClient.Repository mockGitHubRepository() {
+ GithubApplicationClient.Repository repository = mock();
+ when(repository.getDefaultBranch()).thenReturn(MAIN_BRANCH_NAME);
+ when(repository.getName()).thenReturn(REPOSITORY_NAME);
+ when(repository.getFullName()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
+ lenient().when(repository.isPrivate()).thenReturn(true);
+ when(githubApplicationClient.getRepository(DEVOPS_PROJECT_DESCRIPTOR.url(), devOpsAppInstallationToken, DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier())).thenReturn(
+ Optional.of(repository));
+ when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn("generated_" + DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
+ return repository;
+ }
+
+ private ComponentCreationData mockProjectCreation(String projectKey) {
+ ComponentCreationData componentCreationData = mock();
+ ProjectDto projectDto = mockProjectDto(projectKey);
+ when(componentCreationData.projectDto()).thenReturn(projectDto);
+ BranchDto branchDto = mock();
+ when(componentCreationData.mainBranchDto()).thenReturn(branchDto);
+ when(componentUpdater.createWithoutCommit(any(), componentCreationParametersCaptor.capture())).thenReturn(componentCreationData);
+ return componentCreationData;
+ }
+
+ private static ProjectDto mockProjectDto(String projectKey) {
+ ProjectDto projectDto = mock();
+ when(projectDto.getName()).thenReturn(REPOSITORY_NAME);
+ when(projectDto.getKey()).thenReturn(projectKey);
+ when(projectDto.getUuid()).thenReturn("project-uuid-1");
+ return projectDto;
+ }
+
+ private static void assertComponentCreationParametersContainsCorrectInformation(ComponentCreationParameters componentCreationParameters, String expectedKey,
+ CreationMethod expectedCreationMethod) {
+ assertThat(componentCreationParameters.creationMethod()).isEqualTo(expectedCreationMethod);
+ assertThat(componentCreationParameters.mainBranchName()).isEqualTo(MAIN_BRANCH_NAME);
+ assertThat(componentCreationParameters.userLogin()).isEqualTo(USER_LOGIN);
+ assertThat(componentCreationParameters.userUuid()).isEqualTo(USER_UUID);
+
+ NewComponent newComponent = componentCreationParameters.newComponent();
+ assertThat(newComponent.isProject()).isTrue();
+ assertThat(newComponent.qualifier()).isEqualTo(Qualifiers.PROJECT);
+ assertThat(newComponent.key()).isEqualTo(expectedKey);
+ assertThat(newComponent.name()).isEqualTo(REPOSITORY_NAME);
+ }
+
+ private static void assertAlmSettingsDtoContainsCorrectInformation(AlmSettingDto almSettingDto, ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
+ assertThat(projectAlmSettingDto.getAlmRepo()).isEqualTo(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
+ assertThat(projectAlmSettingDto.getAlmSlug()).isNull();
+ assertThat(projectAlmSettingDto.getAlmSettingUuid()).isEqualTo(almSettingDto.getUuid());
+ assertThat(projectAlmSettingDto.getProjectUuid()).isEqualTo(projectDto.getUuid());
+ assertThat(projectAlmSettingDto.getMonorepo()).isFalse();
+ assertThat(projectAlmSettingDto.getSummaryCommentEnabled()).isTrue();
+ }
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactoryTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactoryTest.java
new file mode 100644
index 00000000000..2abdd276753
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorFactoryTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.almsettings.gitlab;
+
+import java.util.Map;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class GitlabProjectCreatorFactoryTest {
+
+ @InjectMocks
+ private GitlabProjectCreatorFactory underTest;
+
+
+ @Test
+ void getDevOpsProjectCreator_withCharacteristics_returnsEmpty() {
+ assertThat(underTest.getDevOpsProjectCreator(mock(DbSession.class), Map.of())).isEmpty();
+ }
+
+
+ @Test
+ void getDevOpsProjectCreator_whenDevOpsPlatformIsNotGitlab_returnsEmpty() {
+ AlmSettingDto almSetting = mock();
+ when(almSetting.getAlm()).thenReturn(ALM.AZURE_DEVOPS);
+ AssertionsForClassTypes.assertThat(underTest.getDevOpsProjectCreator(almSetting, Mockito.mock(DevOpsProjectDescriptor.class))).isEmpty();
+ }
+
+
+ @Test
+ void getDevOpsProjectCreator_whenDevOpsPlatformIsNotGitlab_returnsProjectCreator() {
+ AlmSettingDto almSetting = mock();
+ when(almSetting.getAlm()).thenReturn(ALM.GITLAB);
+ assertThat(underTest.getDevOpsProjectCreator(almSetting, mock(DevOpsProjectDescriptor.class))).isNotEmpty();
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java
new file mode 100644
index 00000000000..76e2d2da308
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.almsettings.gitlab;
+
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.alm.client.gitlab.GitLabBranch;
+import org.sonar.alm.client.gitlab.GitlabApplicationClient;
+import org.sonar.alm.client.gitlab.GitlabServerException;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.ALM;
+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.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.user.UserSession;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class GitlabProjectCreatorTest {
+
+ private static final String PROJECT_UUID = "projectUuid";
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private DbClient dbClient;
+
+ @Mock
+ private ProjectKeyGenerator projectKeyGenerator;
+
+ @Mock
+ private ProjectCreator projectCreator;
+
+ @Mock
+ private AlmSettingDto almSettingDto;
+ @Mock
+ private DevOpsProjectDescriptor devOpsProjectDescriptor;
+ @Mock
+ private GitlabApplicationClient gitlabApplicationClient;
+ @Mock
+ private UserSession userSession;
+
+ @InjectMocks
+ private GitlabProjectCreator underTest;
+
+ private static final String USER_LOGIN = "userLogin";
+ private static final String USER_UUID = "userUuid";
+
+ private static final String GROUP_NAME = "group1";
+ private static final String REPOSITORY_PATH_WITH_NAMESPACE = "pathWith/namespace";
+
+ private static final String GITLAB_PROJECT_NAME = "gitlabProjectName";
+
+ private static final String REPOSITORY_ID = "1234";
+
+ private static final String MAIN_BRANCH_NAME = "defaultBranch";
+
+ private static final String ALM_SETTING_KEY = "gitlab_config_1";
+ private static final String ALM_SETTING_UUID = "almSettingUuid";
+
+ private static final String USER_PAT = "1234";
+
+ public static final String GITLAB_URL = "http://api.com";
+ private static final DevOpsProjectDescriptor DEVOPS_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITLAB, GITLAB_URL, REPOSITORY_ID);
+
+ @BeforeEach
+ void setup() {
+ lenient().when(userSession.getLogin()).thenReturn(USER_LOGIN);
+ lenient().when(userSession.getUuid()).thenReturn(USER_UUID);
+
+ lenient().when(almSettingDto.getUrl()).thenReturn(GITLAB_URL);
+ lenient().when(almSettingDto.getKey()).thenReturn(ALM_SETTING_KEY);
+ lenient().when(almSettingDto.getUuid()).thenReturn(ALM_SETTING_UUID);
+
+ lenient().when(devOpsProjectDescriptor.projectIdentifier()).thenReturn(REPOSITORY_ID);
+ lenient().when(devOpsProjectDescriptor.url()).thenReturn(GITLAB_URL);
+ lenient().when(devOpsProjectDescriptor.alm()).thenReturn(ALM.GITLAB);
+ }
+
+ @Test
+ void isScanAllowedUsingPermissionsFromDevopsPlatform_shouldThrowUnsupportedOperationException() {
+ assertThatExceptionOfType(UnsupportedOperationException.class)
+ .isThrownBy(() -> underTest.isScanAllowedUsingPermissionsFromDevopsPlatform())
+ .withMessage("Not Implemented");
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatform_whenUserHasNoPat_throws() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, false, null, null))
+ .withMessage("personal access token for 'gitlab_config_1' is missing");
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() {
+ mockPatForUser();
+ when(gitlabApplicationClient.getProject(DEVOPS_PROJECT_DESCRIPTOR.url(), USER_PAT, Long.valueOf(REPOSITORY_ID))).thenThrow(new GitlabServerException(404, "Not found"));
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(() -> underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, false, null, null))
+ .withMessage("Failed to fetch GitLab project with ID '1234' from 'http://api.com'");
+
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnGitlab_successfullyCreatesProject() {
+ mockPatForUser();
+ mockGitlabProject();
+ mockMainBranch();
+ mockProjectCreation("projectKey", "projectName");
+
+ underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, true, "projectKey", "projectName");
+
+ ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingCaptor = ArgumentCaptor.forClass(ProjectAlmSettingDto.class);
+
+ verify(dbClient.projectAlmSettingDao()).insertOrUpdate(any(), projectAlmSettingCaptor.capture(), eq(ALM_SETTING_KEY), eq("projectName"), eq("projectKey"));
+
+ ProjectAlmSettingDto createdProjectAlmSettingDto = projectAlmSettingCaptor.getValue();
+
+ assertThat(createdProjectAlmSettingDto.getAlmSettingUuid()).isEqualTo(ALM_SETTING_UUID);
+ assertThat(createdProjectAlmSettingDto.getAlmRepo()).isEqualTo(REPOSITORY_ID);
+ assertThat(createdProjectAlmSettingDto.getProjectUuid()).isEqualTo(PROJECT_UUID);
+ assertThat(createdProjectAlmSettingDto.getMonorepo()).isTrue();
+
+ }
+
+ @Test
+ void createProjectAndBindToDevOpsPlatform_whenNoKeyAndNameSpecified_generatesOneKeyAndUsersGitlabProjectName() {
+ mockPatForUser();
+ mockGitlabProject();
+ mockMainBranch();
+
+ String generatedProjectKey = "generatedProjectKey";
+ when(projectKeyGenerator.generateUniqueProjectKey(REPOSITORY_PATH_WITH_NAMESPACE)).thenReturn(generatedProjectKey);
+
+ mockProjectCreation(generatedProjectKey, GITLAB_PROJECT_NAME);
+
+ underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, true, null, null);
+
+ ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingCaptor = ArgumentCaptor.forClass(ProjectAlmSettingDto.class);
+
+ verify(dbClient.projectAlmSettingDao()).insertOrUpdate(any(), projectAlmSettingCaptor.capture(), eq(ALM_SETTING_KEY), eq(GITLAB_PROJECT_NAME), eq(generatedProjectKey));
+
+ ProjectAlmSettingDto createdProjectAlmSettingDto = projectAlmSettingCaptor.getValue();
+
+ assertThat(createdProjectAlmSettingDto.getAlmSettingUuid()).isEqualTo(ALM_SETTING_UUID);
+ assertThat(createdProjectAlmSettingDto.getAlmRepo()).isEqualTo(REPOSITORY_ID);
+ assertThat(createdProjectAlmSettingDto.getProjectUuid()).isEqualTo(PROJECT_UUID);
+ assertThat(createdProjectAlmSettingDto.getMonorepo()).isTrue();
+ }
+
+ private void mockPatForUser() {
+ AlmPatDto almPatDto = mock();
+ when(almPatDto.getPersonalAccessToken()).thenReturn(USER_PAT);
+ when(dbClient.almPatDao().selectByUserAndAlmSetting(any(), eq(USER_UUID), eq(almSettingDto))).thenReturn(Optional.of(almPatDto));
+ }
+
+ private void mockGitlabProject() {
+ Project project = mock(Project.class);
+ lenient().when(project.getPathWithNamespace()).thenReturn(REPOSITORY_PATH_WITH_NAMESPACE);
+ when(project.getName()).thenReturn(GITLAB_PROJECT_NAME);
+ when(gitlabApplicationClient.getProject(DEVOPS_PROJECT_DESCRIPTOR.url(), USER_PAT, Long.valueOf(REPOSITORY_ID))).thenReturn(project);
+
+ }
+
+ private void mockMainBranch() {
+ when(gitlabApplicationClient.getBranches(DEVOPS_PROJECT_DESCRIPTOR.url(), USER_PAT, Long.valueOf(REPOSITORY_ID)))
+ .thenReturn(List.of(new GitLabBranch("notMain", false), new GitLabBranch(MAIN_BRANCH_NAME, true)));
+ }
+
+ private void mockProjectCreation(String projectKey, String projectName) {
+ ComponentCreationData componentCreationData = mock();
+ ProjectDto projectDto = mock();
+ when(componentCreationData.projectDto()).thenReturn(projectDto);
+ when(projectDto.getUuid()).thenReturn(PROJECT_UUID);
+ when(projectDto.getKey()).thenReturn(projectKey);
+ when(projectDto.getName()).thenReturn(projectName);
+ when(projectCreator.createProject(any(), eq(projectKey), eq(projectName), eq(MAIN_BRANCH_NAME), eq(CreationMethod.ALM_IMPORT_API)))
+ .thenReturn(componentCreationData);
+ }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/CaycUtilsTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/CaycUtilsTest.java
new file mode 100644
index 00000000000..0afe761eeaa
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/CaycUtilsTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.newcodeperiod;
+
+import org.junit.Test;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class CaycUtilsTest {
+
+ @Test
+ public void reference_branch_is_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.REFERENCE_BRANCH)
+ .setValue("master");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isTrue();
+ }
+
+ @Test
+ public void previous_version_is_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.PREVIOUS_VERSION)
+ .setValue("1.0");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isTrue();
+ }
+
+ @Test
+ public void number_of_days_smaller_than_90_is_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.NUMBER_OF_DAYS)
+ .setValue("30");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isTrue();
+ }
+
+ @Test
+ public void number_of_days_smaller_than_1_is_not_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.NUMBER_OF_DAYS)
+ .setValue("0");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isFalse();
+ }
+
+ @Test
+ public void number_of_days_bigger_than_90_is_not_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.NUMBER_OF_DAYS)
+ .setValue("91");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isFalse();
+ }
+
+ @Test
+ public void specific_analysis_is_compliant() {
+ var newCodePeriod = new NewCodePeriodDto()
+ .setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
+ .setValue("sdfsafsdf");
+ assertThat(CaycUtils.isNewCodePeriodCompliant(newCodePeriod.getType(), newCodePeriod.getValue())).isTrue();
+ }
+
+ @Test
+ public void wrong_number_of_days_format_should_throw_exception() {
+ assertThatThrownBy(() -> CaycUtils.isNewCodePeriodCompliant(NewCodePeriodType.NUMBER_OF_DAYS, "abc"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Failed to parse number of days: abc");
+ }
+} \ No newline at end of file
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolverTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolverTest.java
new file mode 100644
index 00000000000..6bba3336ad9
--- /dev/null
+++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/newcodeperiod/NewCodeDefinitionResolverTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.newcodeperiod;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.core.platform.PlatformEditionProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
+
+public class NewCodeDefinitionResolverTest {
+
+ private static final String MAIN_BRANCH_UUID = "main-branch-uuid";
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private static final String DEFAULT_PROJECT_ID = "12345";
+
+ private static final String MAIN_BRANCH = "main";
+
+ private DbSession dbSession = db.getSession();
+ private DbClient dbClient = db.getDbClient();
+ private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
+ private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_no_valid_type() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, "nonValid", null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid type: nonValid");
+ }
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_type_is_not_allowed() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, SPECIFIC_ANALYSIS.name(), null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid type 'SPECIFIC_ANALYSIS'. `newCodeDefinitionType` can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH]");
+ }
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_no_value_for_days() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, NUMBER_OF_DAYS.name(), null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("New code definition type 'NUMBER_OF_DAYS' requires a newCodeDefinitionValue");
+ }
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_days_is_invalid() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, NUMBER_OF_DAYS.name(), "unknown"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Failed to parse number of days: unknown");
+ }
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_value_is_set_for_reference_branch() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, REFERENCE_BRANCH.name(), "feature/zw"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Unexpected value for newCodeDefinitionType 'REFERENCE_BRANCH'");
+ }
+
+ @Test
+ public void createNewCodeDefinition_throw_IAE_if_previous_version_type_and_value_provided() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, PREVIOUS_VERSION.name(), "10.2.3"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Unexpected value for newCodeDefinitionType 'PREVIOUS_VERSION'");
+ }
+
+ @Test
+ public void createNewCodeDefinition_persist_previous_version_type() {
+ newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, PREVIOUS_VERSION.name(), null);
+
+ Optional<NewCodePeriodDto> newCodePeriodDto = dbClient.newCodePeriodDao().selectByProject(dbSession, DEFAULT_PROJECT_ID);
+ assertThat(newCodePeriodDto).map(NewCodePeriodDto::getType).hasValue(PREVIOUS_VERSION);
+ }
+
+ @Test
+ public void createNewCodeDefinition_return_days_value_for_number_of_days_type() {
+ String numberOfDays = "30";
+
+ newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, NUMBER_OF_DAYS.name(), numberOfDays);
+
+ Optional<NewCodePeriodDto> newCodePeriodDto = dbClient.newCodePeriodDao().selectByProject(dbSession, DEFAULT_PROJECT_ID);
+
+ assertThat(newCodePeriodDto)
+ .isPresent()
+ .get()
+ .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
+ .containsExactly(NUMBER_OF_DAYS, numberOfDays);
+ }
+
+ @Test
+ public void createNewCodeDefinition_return_branch_value_for_reference_branch_type() {
+ newCodeDefinitionResolver.createNewCodeDefinition(dbSession, DEFAULT_PROJECT_ID, MAIN_BRANCH_UUID, MAIN_BRANCH, REFERENCE_BRANCH.name(), null);
+
+ Optional<NewCodePeriodDto> newCodePeriodDto = dbClient.newCodePeriodDao().selectByProject(dbSession, DEFAULT_PROJECT_ID);
+
+ assertThat(newCodePeriodDto)
+ .isPresent()
+ .get()
+ .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid, NewCodePeriodDto::getProjectUuid)
+ .containsExactly(REFERENCE_BRANCH, MAIN_BRANCH, null, DEFAULT_PROJECT_ID);
+ }
+
+ @Test
+ public void checkNewCodeDefinitionParam_throw_IAE_if_newCodeDefinitionValue_is_provided_without_newCodeDefinitionType() {
+ assertThatThrownBy(() -> newCodeDefinitionResolver.checkNewCodeDefinitionParam(null, "anyvalue"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("New code definition type is required when new code definition value is provided");
+ }
+
+ @Test
+ public void checkNewCodeDefinitionParam_do_not_throw_when_both_value_and_type_are_provided() {
+ assertThatNoException()
+ .isThrownBy(() -> newCodeDefinitionResolver.checkNewCodeDefinitionParam("PREVIOUS_VERSION", "anyvalue"));
+ }
+
+}