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.ManagedInstanceService;
59 import org.sonar.server.management.ManagedProjectService;
60 import org.sonar.server.permission.PermissionService;
61 import org.sonar.server.permission.PermissionServiceImpl;
62 import org.sonar.server.permission.PermissionUpdater;
63 import org.sonar.server.permission.UserPermissionChange;
64 import org.sonar.server.project.ProjectDefaultVisibility;
65 import org.sonar.server.project.ws.ProjectCreator;
66 import org.sonar.server.user.UserSession;
68 import static java.util.Objects.requireNonNull;
69 import static java.util.stream.Collectors.toSet;
70 import static org.assertj.core.api.Assertions.assertThat;
71 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
72 import static org.mockito.ArgumentMatchers.any;
73 import static org.mockito.ArgumentMatchers.eq;
74 import static org.mockito.Mockito.mock;
75 import static org.mockito.Mockito.verify;
76 import static org.mockito.Mockito.when;
77 import static org.sonar.db.project.CreationMethod.ALM_IMPORT_API;
78 import static org.sonar.db.project.CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG;
80 @RunWith(MockitoJUnitRunner.class)
81 public class GithubProjectCreatorTest {
83 private static final String ORGANIZATION_NAME = "orga2";
84 private static final String REPOSITORY_NAME = "repo1";
86 private static final String MAIN_BRANCH_NAME = "defaultBranch";
87 private static final DevOpsProjectDescriptor DEVOPS_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, "http://api.com", ORGANIZATION_NAME + "/" + REPOSITORY_NAME);
88 private static final String ALM_SETTING_KEY = "github_config_1";
89 private static final String USER_LOGIN = "userLogin";
90 private static final String USER_UUID = "userUuid";
91 private static final String BRANCH_UUID = "branchUuid1";
93 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
94 private DbClient dbClient;
96 private GithubApplicationClient githubApplicationClient;
98 private GithubPermissionConverter githubPermissionConverter;
100 private ProjectKeyGenerator projectKeyGenerator;
102 private ComponentUpdater componentUpdater;
104 private GithubProjectCreationParameters githubProjectCreationParameters;
106 private AccessToken devOpsAppInstallationToken;
108 private AppInstallationToken authAppInstallationToken;
110 private UserSession userSession;
112 private AlmSettingDto almSettingDto;
113 private final PermissionService permissionService = new PermissionServiceImpl(mock());
115 private PermissionUpdater<UserPermissionChange> permissionUpdater;
117 private ManagedProjectService managedProjectService;
119 private ManagedInstanceService managedInstanceService;
121 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
122 private ProjectDefaultVisibility projectDefaultVisibility;
123 private ProjectCreator projectCreator;
125 private GithubProjectCreator githubProjectCreator;
128 ArgumentCaptor<ComponentCreationParameters> componentCreationParametersCaptor;
130 ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingDtoCaptor;
133 public void setup() {
134 when(userSession.getLogin()).thenReturn(USER_LOGIN);
135 when(userSession.getUuid()).thenReturn(USER_UUID);
137 when(almSettingDto.getUrl()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.url());
138 when(almSettingDto.getKey()).thenReturn(ALM_SETTING_KEY);
140 when(githubProjectCreationParameters.devOpsProjectDescriptor()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR);
141 when(githubProjectCreationParameters.userSession()).thenReturn(userSession);
142 when(githubProjectCreationParameters.devOpsAppInstallationToken()).thenReturn(devOpsAppInstallationToken);
143 when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(authAppInstallationToken);
144 when(githubProjectCreationParameters.almSettingDto()).thenReturn(almSettingDto);
146 projectCreator = new ProjectCreator(userSession, projectDefaultVisibility, managedInstanceService, componentUpdater);
147 githubProjectCreator = new GithubProjectCreator(dbClient, githubApplicationClient, githubPermissionConverter, projectKeyGenerator,
148 permissionUpdater, permissionService, managedProjectService, projectCreator, githubProjectCreationParameters);
153 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenNoAuthToken_throws() {
154 when(githubProjectCreationParameters.authAppInstallationToken()).thenReturn(null);
156 assertThatIllegalStateException().isThrownBy(() -> githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform())
157 .withMessage("An auth app token is required in case repository permissions checking is necessary.");
161 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenUserIsNotAGitHubUser_returnsFalse() {
162 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
166 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccessButNoScanPermissions_returnsFalse() {
167 GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
168 mockGithubCollaboratorsFromApi(collaborator1);
169 bindSessionToCollaborator(collaborator1);
171 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
175 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenCollaboratorHasDirectAccess_returnsTrue() {
176 GsonRepositoryCollaborator collaborator1 = mockCollaborator("collaborator1", 1, "role1", "read", "admin");
177 GsonRepositoryCollaborator collaborator2 = mockCollaborator("collaborator2", 2, "role2", "read", "scan");
178 mockGithubCollaboratorsFromApi(collaborator1, collaborator2);
179 bindSessionToCollaborator(collaborator2);
181 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
185 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButNoScanPermissions_returnsFalse() {
186 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.ADMIN);
187 mockTeamsFromApi(team2);
188 bindGroupsToUser(team2.name());
190 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
194 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeam_returnsTrue() {
195 GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
196 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
197 mockTeamsFromApi(team1, team2);
198 bindGroupsToUser(team1.name(), team2.name());
200 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isTrue();
204 public void isScanAllowedUsingPermissionsFromDevopsPlatform_whenAccessViaTeamButUserNotInTeam_returnsFalse() {
205 GsonRepositoryTeam team1 = mockGithubTeam("team1", 1, "role1", "read", "another_perm");
206 GsonRepositoryTeam team2 = mockGithubTeam("team2", 2, "role2", "another_perm", UserRole.SCAN);
207 mockTeamsFromApi(team1, team2);
208 bindGroupsToUser(team1.name());
210 assertThat(githubProjectCreator.isScanAllowedUsingPermissionsFromDevopsPlatform()).isFalse();
213 private void bindSessionToCollaborator(GsonRepositoryCollaborator collaborator1) {
214 UserSession.ExternalIdentity externalIdentity = new UserSession.ExternalIdentity(String.valueOf(collaborator1.id()), collaborator1.name());
215 when(userSession.getExternalIdentity()).thenReturn(Optional.of(externalIdentity));
218 private GsonRepositoryCollaborator mockCollaborator(String collaboratorLogin, int id, String role1, String... sqPermissions) {
219 GsonRepositoryCollaborator collaborator = new GsonRepositoryCollaborator(collaboratorLogin, id, role1,
220 new GsonRepositoryPermissions(false, false, false, false, false));
221 mockPermissionsConversion(collaborator, sqPermissions);
225 private void mockGithubCollaboratorsFromApi(GsonRepositoryCollaborator... repositoryCollaborators) {
226 Set<GsonRepositoryCollaborator> collaborators = Arrays.stream(repositoryCollaborators).collect(toSet());
227 when(githubApplicationClient.getRepositoryCollaborators(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME)).thenReturn(
231 private GsonRepositoryTeam mockGithubTeam(String name, int id, String role, String... sqPermissions) {
232 GsonRepositoryTeam gsonRepositoryTeam = new GsonRepositoryTeam(name, id, name + "slug", role, new GsonRepositoryPermissions(false, false, false, false, false));
233 mockPermissionsConversion(gsonRepositoryTeam, sqPermissions);
234 return gsonRepositoryTeam;
237 private void mockTeamsFromApi(GsonRepositoryTeam... repositoryTeams) {
238 when(githubApplicationClient.getRepositoryTeams(DEVOPS_PROJECT_DESCRIPTOR.url(), authAppInstallationToken, ORGANIZATION_NAME, REPOSITORY_NAME))
239 .thenReturn(Arrays.stream(repositoryTeams).collect(toSet()));
242 private void mockPermissionsConversion(GsonRepositoryCollaborator collaborator, String... sqPermissions) {
243 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
244 when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, collaborator.roleName(), collaborator.permissions()))
245 .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
248 private void mockPermissionsConversion(GsonRepositoryTeam team, String... sqPermissions) {
249 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = mockPermissionsMappingsDtos();
250 when(githubPermissionConverter.toSonarqubeRolesWithFallbackOnRepositoryPermissions(githubPermissionsMappingDtos, team.permission(), team.permissions()))
251 .thenReturn(Arrays.stream(sqPermissions).collect(toSet()));
254 private Set<GithubPermissionsMappingDto> mockPermissionsMappingsDtos() {
255 Set<GithubPermissionsMappingDto> githubPermissionsMappingDtos = Set.of(mock(GithubPermissionsMappingDto.class));
256 when(dbClient.githubPermissionsMappingDao().findAll(any())).thenReturn(githubPermissionsMappingDtos);
257 return githubPermissionsMappingDtos;
260 private void bindGroupsToUser(String... groupNames) {
261 Set<GroupDto> groupDtos = Arrays.stream(groupNames)
262 .map(groupName -> new GroupDto().setName(ORGANIZATION_NAME + "/" + groupName).setUuid("uuid_" + groupName))
264 when(userSession.getGroups()).thenReturn(groupDtos);
268 public void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() {
269 assertThatIllegalStateException().isThrownBy(
270 () -> githubProjectCreator.createProjectAndBindToDevOpsPlatform(mock(), SCANNER_API_DEVOPS_AUTO_CONFIG, null))
271 .withMessage("Impossible to find the repository 'orga2/repo1' on GitHub, using the devops config " + ALM_SETTING_KEY);
275 public void createProjectAndBindToDevOpsPlatformFromScanner_whenRepoFoundOnGitHub_successfullyCreatesProject() {
277 mockGitHubRepository();
279 ComponentCreationData componentCreationData = mockProjectCreation("generated_orga2/repo1");
280 ProjectAlmSettingDao projectAlmSettingDao = mock();
281 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
284 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true),
285 SCANNER_API_DEVOPS_AUTO_CONFIG, null);
288 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
290 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
291 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, "generated_orga2/repo1", SCANNER_API_DEVOPS_AUTO_CONFIG);
292 assertThat(componentCreationParameters.isManaged()).isFalse();
293 assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();
295 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq("generated_orga2/repo1"));
296 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
297 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
301 public void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHub_successfullyCreatesProject() {
303 String projectKey = "customProjectKey";
304 mockGitHubRepository();
306 ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
307 ProjectAlmSettingDao projectAlmSettingDao = mock();
308 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
311 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, projectKey);
314 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
316 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
317 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
318 assertThat(componentCreationParameters.isManaged()).isFalse();
319 assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();
321 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
322 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
323 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
327 private ArgumentCaptor<Collection<UserPermissionChange>> permissionChangesCaptor;
330 public void createProjectAndBindToDevOpsPlatformFromApi_whenRepoFoundOnGitHubAutoProvisioningOnAndRepoPrivate_successfullyCreatesProject() {
332 when(projectDefaultVisibility.get(any()).isPrivate()).thenReturn(true);
333 when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
335 String projectKey = "customProjectKey";
336 mockGitHubRepository();
338 ComponentCreationData componentCreationData = mockProjectCreation(projectKey);
339 ProjectAlmSettingDao projectAlmSettingDao = mock();
340 when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);
343 ComponentCreationData actualComponentCreationData = githubProjectCreator.createProjectAndBindToDevOpsPlatform(dbClient.openSession(true), ALM_IMPORT_API, projectKey);
346 assertThat(actualComponentCreationData).isEqualTo(componentCreationData);
348 ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
349 assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters, projectKey, ALM_IMPORT_API);
350 assertThat(componentCreationParameters.isManaged()).isTrue();
351 assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();
353 verifyScanPermissionWasAddedToUser(actualComponentCreationData);
354 verifyProjectSyncTaskWasCreated(actualComponentCreationData);
356 verify(projectAlmSettingDao).insertOrUpdate(any(), projectAlmSettingDtoCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(projectKey));
357 ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
358 assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);
361 private void verifyProjectSyncTaskWasCreated(ComponentCreationData componentCreationData) {
362 String projectUuid = requireNonNull(componentCreationData.projectDto()).getUuid();
363 String mainBranchUuid = requireNonNull(componentCreationData.mainBranchDto()).getUuid();
364 verify(managedProjectService).queuePermissionSyncTask(USER_UUID, mainBranchUuid, projectUuid);
367 private void verifyScanPermissionWasAddedToUser(ComponentCreationData actualComponentCreationData) {
368 verify(permissionUpdater).apply(any(), permissionChangesCaptor.capture());
369 UserPermissionChange permissionChange = permissionChangesCaptor.getValue().iterator().next();
370 assertThat(permissionChange.getUserId().getUuid()).isEqualTo(userSession.getUuid());
371 assertThat(permissionChange.getUserId().getLogin()).isEqualTo(userSession.getLogin());
372 assertThat(permissionChange.getPermission()).isEqualTo(UserRole.SCAN);
373 assertThat(permissionChange.getProjectUuid()).isEqualTo(actualComponentCreationData.projectDto().getUuid());
376 private void mockGitHubRepository() {
377 GithubApplicationClient.Repository repository = mock();
378 when(repository.getDefaultBranch()).thenReturn(MAIN_BRANCH_NAME);
379 when(repository.getName()).thenReturn(REPOSITORY_NAME);
380 when(repository.getFullName()).thenReturn(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
381 when(githubApplicationClient.getRepository(DEVOPS_PROJECT_DESCRIPTOR.url(), devOpsAppInstallationToken, DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier())).thenReturn(
382 Optional.of(repository));
383 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn("generated_" + DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
386 private ComponentCreationData mockProjectCreation(String projectKey) {
387 ComponentCreationData componentCreationData = mock();
388 ProjectDto projectDto = mockProjectDto(projectKey);
389 when(componentCreationData.projectDto()).thenReturn(projectDto);
390 BranchDto branchDto = mock();
391 when(branchDto.getUuid()).thenReturn(BRANCH_UUID);
392 when(componentCreationData.mainBranchDto()).thenReturn(branchDto);
393 when(componentUpdater.createWithoutCommit(any(), componentCreationParametersCaptor.capture())).thenReturn(componentCreationData);
394 return componentCreationData;
397 private static ProjectDto mockProjectDto(String projectKey) {
398 ProjectDto projectDto = mock();
399 when(projectDto.getName()).thenReturn(REPOSITORY_NAME);
400 when(projectDto.getKey()).thenReturn(projectKey);
401 when(projectDto.getUuid()).thenReturn("project-uuid-1");
405 private static void assertComponentCreationParametersContainsCorrectInformation(ComponentCreationParameters componentCreationParameters, String expectedKey,
406 CreationMethod expectedCreationMethod) {
407 assertThat(componentCreationParameters.creationMethod()).isEqualTo(expectedCreationMethod);
408 assertThat(componentCreationParameters.mainBranchName()).isEqualTo(MAIN_BRANCH_NAME);
409 assertThat(componentCreationParameters.userLogin()).isEqualTo(USER_LOGIN);
410 assertThat(componentCreationParameters.userUuid()).isEqualTo(USER_UUID);
412 NewComponent newComponent = componentCreationParameters.newComponent();
413 assertThat(newComponent.isProject()).isTrue();
414 assertThat(newComponent.qualifier()).isEqualTo(Qualifiers.PROJECT);
415 assertThat(newComponent.key()).isEqualTo(expectedKey);
416 assertThat(newComponent.name()).isEqualTo(REPOSITORY_NAME);
419 private static void assertAlmSettingsDtoContainsCorrectInformation(AlmSettingDto almSettingDto, ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
420 assertThat(projectAlmSettingDto.getAlmRepo()).isEqualTo(DEVOPS_PROJECT_DESCRIPTOR.projectIdentifier());
421 assertThat(projectAlmSettingDto.getAlmSlug()).isNull();
422 assertThat(projectAlmSettingDto.getAlmSettingUuid()).isEqualTo(almSettingDto.getUuid());
423 assertThat(projectAlmSettingDto.getProjectUuid()).isEqualTo(projectDto.getUuid());
424 assertThat(projectAlmSettingDto.getMonorepo()).isFalse();
425 assertThat(projectAlmSettingDto.getSummaryCommentEnabled()).isTrue();