3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.almsettings.ws;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Optional;
26 import org.junit.Before;
27 import org.junit.Test;
28 import org.junit.runner.RunWith;
29 import org.mockito.Answers;
30 import org.mockito.ArgumentCaptor;
31 import org.mockito.Captor;
32 import org.mockito.Mock;
33 import org.mockito.junit.MockitoJUnitRunner;
34 import org.sonar.alm.client.github.AppInstallationToken;
35 import org.sonar.alm.client.github.GithubApplicationClient;
36 import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
37 import org.sonar.alm.client.github.api.GsonRepositoryTeam;
38 import org.sonar.alm.client.github.security.AccessToken;
39 import org.sonar.api.resources.Qualifiers;
40 import org.sonar.api.web.UserRole;
41 import org.sonar.alm.client.github.GithubPermissionConverter;
42 import org.sonar.auth.github.GsonRepositoryPermissions;
43 import org.sonar.db.DbClient;
44 import org.sonar.db.alm.setting.ALM;
45 import org.sonar.db.alm.setting.AlmSettingDto;
46 import org.sonar.db.alm.setting.ProjectAlmSettingDao;
47 import org.sonar.db.alm.setting.ProjectAlmSettingDto;
48 import org.sonar.db.component.BranchDto;
49 import org.sonar.db.project.CreationMethod;
50 import org.sonar.db.project.ProjectDto;
51 import org.sonar.db.provisioning.GithubPermissionsMappingDto;
52 import org.sonar.db.user.GroupDto;
53 import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
54 import org.sonar.server.component.ComponentCreationData;
55 import org.sonar.server.component.ComponentCreationParameters;
56 import org.sonar.server.component.ComponentUpdater;
57 import org.sonar.server.component.NewComponent;
58 import org.sonar.server.management.ManagedProjectService;
59 import org.sonar.server.permission.PermissionService;
60 import org.sonar.server.permission.PermissionServiceImpl;
61 import org.sonar.server.permission.PermissionUpdater;
62 import org.sonar.server.permission.UserPermissionChange;
63 import org.sonar.server.user.UserSession;
65 import static java.util.Objects.requireNonNull;
66 import static java.util.stream.Collectors.toSet;
67 import static org.assertj.core.api.Assertions.assertThat;
68 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
69 import static org.mockito.ArgumentMatchers.any;
70 import static org.mockito.ArgumentMatchers.eq;
71 import static org.mockito.Mockito.mock;
72 import static org.mockito.Mockito.verify;
73 import static org.mockito.Mockito.when;
74 import static org.sonar.db.project.CreationMethod.ALM_IMPORT_API;
75 import static org.sonar.db.project.CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG;
77 @RunWith(MockitoJUnitRunner.class)
78 public class GithubProjectCreatorTest {
80 private static final String ORGANIZATION_NAME = "orga2";
81 private static final String REPOSITORY_NAME = "repo1";
83 private static final String MAIN_BRANCH_NAME = "defaultBranch";
84 private static final DevOpsProjectDescriptor DEVOPS_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, "http://api.com", ORGANIZATION_NAME + "/" + REPOSITORY_NAME);
85 private static final String ALM_SETTING_KEY = "github_config_1";
86 private static final String USER_LOGIN = "userLogin";
87 private static final String USER_UUID = "userUuid";
88 private static final String BRANCH_UUID = "branchUuid1";
90 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
91 private DbClient dbClient;
93 private GithubApplicationClient githubApplicationClient;
95 private GithubPermissionConverter githubPermissionConverter;
97 private ProjectKeyGenerator projectKeyGenerator;
99 private ComponentUpdater componentUpdater;
101 private GithubProjectCreationParameters githubProjectCreationParameters;
103 private AccessToken devOpsAppInstallationToken;
105 private AppInstallationToken authAppInstallationToken;
107 private UserSession userSession;
109 private AlmSettingDto almSettingDto;
110 private final PermissionService permissionService = new PermissionServiceImpl(mock());
112 private PermissionUpdater<UserPermissionChange> permissionUpdater;
115 private ManagedProjectService managedProjectService;
117 private GithubProjectCreator githubProjectCreator;
120 ArgumentCaptor<ComponentCreationParameters> componentCreationParametersCaptor;
122 ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingDtoCaptor;
125 public void setup() {
126 when(userSession.getLogin()).thenReturn(USER_LOGIN);
127 when(userSession.getUuid()).thenReturn(USER_UUID);
129 when(almSettingDto.getUrl()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.url());
130 when(almSettingDto.getKey()).thenReturn(ALM_SETTING_KEY);
132 when(githubProjectCreationParameters.devOpsProjectDescriptor()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR);
133 when(githubProjectCreationParameters.userSession()).thenReturn(userSession);
134 when(githubProjectCreationParameters.devOpsAppInstallationToken()).thenReturn(devOpsAppInstallationToken);
135 when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(authAppInstallationToken);
136 when(githubProjectCreationParameters.almSettingDto()).thenReturn(almSettingDto);
138 githubProjectCreator = new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator, componentUpdater,
139 permissionUpdater, permissionService, managedProjectService, githubProjectCreationParameters);
143 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenNoAuthToken_throws() {
144 when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(null);
146 assertThatIllegalStateException().isThrownBy(() -> githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform())
147 .withMessage("An auth app token is required in case repository permissions checking is necessary.");
150 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsNotAGitHubUser_returnsFalse() {
151 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
155 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccessButNoScanPermissions_returnsFalse() {
156 GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
157 mockGithubCollaboratorsFromApi(collaborator1);
158 bindSessionToCollaborator(collaborator1);
160 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
164 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccess_returnsTrue() {
165 GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
166 GsonRepositoryCollaborator collaborator2 = mockCollaborator("collaborator2", 2, "role2", "read", "scan");
167 mockGithubCollaboratorsFromApi(collaborator1, collaborator2);
168 bindSessionToCollaborator(collaborator2);
170 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
174 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButNoScanPermissions_returnsFalse() {
175 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.ADMIN);
176 mockTeamsFromApi(team2);
177 bindGroupsToUser(team2.name());
179 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
183 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeam_returnsTrue() {
184 GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
185 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
186 mockTeamsFromApi(team1, team2);
187 bindGroupsToUser(team1.name(), team2.name());
189 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
193 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButUserNotInTeam_returnsFalse() {
194 GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
195 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
196 mockTeamsFromApi(team1, team2);
197 bindGroupsToUser(team1.name());
199 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
202 private void bindSessionToCollaborator(GsonRepositoryCollaborator collaborator1) {
203 UserSession.ExternalIdentity externalIdentity = new UserSession.ExternalIdentity(String.valueOf(collaborator1.id()), collaborator1.name());
204 when(userSession.getExternalIdentity()).thenReturn(Optional.of(externalIdentity));
207 private GsonRepositoryCollaborator mockCollaborator(String collaboratorLogin, int id, String role1, String... sqPermissions) {
208 GsonRepositoryCollaborator collaborator = new GsonRepositoryCollaborator(collaboratorLogin, id, role1,
209 new GsonRepositoryPermissions(false, false, false, false, false));
210 mockPermissionsConversion(collaborator, sqPermissions);
214 private void mockGithubCollaboratorsFromApi(GsonRepositoryCollaborator... repositoryCollaborators) {
215 Set<GsonRepositoryCollaborator> collaborators = Arrays.stream(repositoryCollaborators).collect(toSet());
216 when(githubApplicationClient.getRepositoryCollaborators(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME)).thenReturn(collaborators);
219 private GsonRepositoryTeam mockGithubTeam(String name, int id, String role, String... sqPermissions) {
220 GsonRepositoryTeam gsonRepositoryTeam = new GsonRepositoryTeam(name, id, name + "slug", role, new GsonRepositoryPermissions(false, false, false, false, false));
221 mockPermissionsConversion(gsonRepositoryTeam, sqPermissions);
222 return gsonRepositoryTeam;
225 private void mockTeamsFromApi(GsonRepositoryTeam... repositoryTeams) {
226 when(githubApplicationClient.getRepositoryTeams(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME))
227 .thenReturn(Arrays.stream(repositoryTeams).collect(toSet()));
230 private void mockPermissionsConversion(GsonRepositoryCollaborator collaborator, String... sqPermissions) {
231 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
232 when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, collaborator.roleName(), collaborator.permissions()))
233 .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
236 private void mockPermissionsConversion(GsonRepositoryTeam team, String... sqPermissions) {
237 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
238 when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, team.permission(), team.permissions()))
239 .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
242 private Set<GithubPermissionsMappingDto> mockPermissionsMappingsDtos() {
243 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = Set.of(mock(GithubPermissionsMappingDto.class));
244 when(dbClient.githubPermissionsMappingDao().findAll(any())).thenReturn(githubPermissionsMappingDtos);
245 return githubPermissionsMappingDtos;
248 private void bindGroupsToUser(String... groupNames) {
249 Set<GroupDto> groupDtos = Arrays.stream(groupNames)
250 .map(groupName -> new GroupDto().setName(ORGANIZATION_NAME + "/" + groupName).setUuid("uuid_" + groupName))
252 when(userSession.getGroups()).thenReturn(groupDtos);
256 public void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() {
257 assertThatIllegalStateException().isThrownBy(
258 () -> githubProjectCreator.createProjectAndBindToDevOpsPlatform(mock(), SCANNER_API_DEVOPS_AUTO_CONFIG, null))
259 .withMessage("Impossible to find the repository 'orga2/repo1' on GitHub, using the devops config " + ALM_SETTING_KEY);
263 public void createProjectAndBindToDevOpsPlatformFromScanner_whenRepoFoundOnGitHub_successfullyCreatesProject() {
265 mockGitHubRepository();
267 ComponentCreationData componentCreationData = mockProjectCreation("generated_orga2/repo1");
268 ProjectAlmSettingDao projectAlmSettingDao = mock();
269 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
272 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true),
273 SCANNER_API_DEVOPS_AUTO_CONFIG, null);
276 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
278 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
279 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, "generated_orga2/repo1", SCANNER_API_DEVOPS_AUTO_CONFIG);
280 assertThat(componentCreationParameters.isManaged()).isFalse();
281 assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();
283 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq("generated_orga2/repo1"));
284 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
285 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
289 public void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHub_successfullyCreatesProject() {
291 String projectKey = "customProjectKey";
292 mockGitHubRepository();
294 ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
295 ProjectAlmSettingDao projectAlmSettingDao = mock();
296 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
299 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, projectKey);
302 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
304 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
305 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
306 assertThat(componentCreationParameters.isManaged()).isFalse();
307 assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();
309 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
310 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
311 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
315 private ArgumentCaptor<Collection<UserPermissionChange>> permissionChangesCaptor;
318 public void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHubAutoProvisioningOnAndRepoPrivate_successfullyCreatesProject() {
320 when(githubProjectCreationParameters.projectsArePrivateByDefault()).thenReturn(true);
321 when(githubProjectCreationParameters.isProvisioningEnabled()).thenReturn(true);
323 String projectKey = "customProjectKey";
324 mockGitHubRepository();
326 ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
327 ProjectAlmSettingDao projectAlmSettingDao = mock();
328 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
331 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, projectKey);
334 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
336 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
337 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
338 assertThat(componentCreationParameters.isManaged()).isTrue();
339 assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
341 verifyScanPermissionWasAddedToUser(actualComponentCreationData);
342 verifyProjectSyncTaskWasCreated(actualComponentCreationData);
344 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
345 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
346 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
349 private void verifyProjectSyncTaskWasCreated(ComponentCreationData componentCreationData) {
350 String projectUuid = requireNonNull( componentCreationData.projectDto()).getUuid();
351 String mainBranchUuid = requireNonNull(componentCreationData.mainBranchDto()).getUuid();
352 verify(managedProjectService).queuePermissionSyncTask(USER_UUID, mainBranchUuid, projectUuid);
355 private void verifyScanPermissionWasAddedToUser(ComponentCreationData actualComponentCreationData) {
356 verify(permissionUpdater).apply(any(), permissionChangesCaptor.capture());
357 UserPermissionChange permissionChange = permissionChangesCaptor.getValue().iterator().next();
358 assertThat(permissionChange.getUserId().getUuid()).isEqualTo(userSession.getUuid());
359 assertThat(permissionChange.getUserId().getLogin()).isEqualTo(userSession.getLogin());
360 assertThat(permissionChange.getPermission()).isEqualTo(UserRole.SCAN);
361 assertThat(permissionChange.getProjectUuid()).isEqualTo(actualComponentCreationData.projectDto().getUuid());
364 private void mockGitHubRepository() {
365 GithubApplicationClient.Repository repository = mock();
366 when(repository.getDefaultBranch()).thenReturn(MAIN_BRANCH_NAME);
367 when(repository.getName()).thenReturn(REPOSITORY_NAME);
368 when(repository.getFullName()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
369 when(githubApplicationClient.getRepository(DEVOPS_PROJECT_DESCRIPTOR.url(), devOpsAppInstallationToken, DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier())).thenReturn(
370 Optional.of(repository));
371 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn("generated_" + DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
374 private ComponentCreationData mockProjectCreation(String projectKey) {
375 ComponentCreationData componentCreationData = mock();
376 ProjectDto projectDto = mockProjectDto(projectKey);
377 when(componentCreationData.projectDto()).thenReturn(projectDto);
378 BranchDto branchDto = mock();
379 when(branchDto.getUuid()).thenReturn(BRANCH_UUID);
380 when(componentCreationData.mainBranchDto()).thenReturn(branchDto);
381 when(componentUpdater.createWithoutCommit(any(), componentCreationParametersCaptor.capture())).thenReturn(componentCreationData);
382 return componentCreationData;
385 private static ProjectDto mockProjectDto(String projectKey) {
386 ProjectDto projectDto = mock();
387 when(projectDto.getName()).thenReturn(REPOSITORY_NAME);
388 when(projectDto.getKey()).thenReturn(projectKey);
389 when(projectDto.getUuid()).thenReturn("project-uuid-1");
393 private static void assertComponentCreationParametersContainsCorrectInformation(ComponentCreationParameters componentCreationParameters, String expectedKey,
394 CreationMethod expectedCreationMethod) {
395 assertThat(componentCreationParameters.creationMethod()).isEqualTo(expectedCreationMethod);
396 assertThat(componentCreationParameters.mainBranchName()).isEqualTo(MAIN_BRANCH_NAME);
397 assertThat(componentCreationParameters.userLogin()).isEqualTo(USER_LOGIN);
398 assertThat(componentCreationParameters.userUuid()).isEqualTo(USER_UUID);
400 NewComponent newComponent = componentCreationParameters.newComponent();
401 assertThat(newComponent.isProject()).isTrue();
402 assertThat(newComponent.qualifier()).isEqualTo(Qualifiers.PROJECT);
403 assertThat(newComponent.key()).isEqualTo(expectedKey);
404 assertThat(newComponent.name()).isEqualTo(REPOSITORY_NAME);
407 private static void assertAlmSettingsDtoContainsCorrectInformation(AlmSettingDto almSettingDto, ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
408 assertThat(projectAlmSettingDto.getAlmRepo()).isEqualTo(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
409 assertThat(projectAlmSettingDto.getAlmSlug()).isNull();
410 assertThat(projectAlmSettingDto.getAlmSettingUuid()).isEqualTo(almSettingDto.getUuid());
411 assertThat(projectAlmSettingDto.getProjectUuid()).isEqualTo(projectDto.getUuid());
412 assertThat(projectAlmSettingDto.getMonorepo()).isFalse();
413 assertThat(projectAlmSettingDto.getSummaryCommentEnabled()).isTrue();