From cb16d8a222d99630826a64ba891dc93b77187bfc Mon Sep 17 00:00:00 2001 From: Zipeng WU Date: Fri, 14 May 2021 17:42:12 +0200 Subject: [PATCH] SONAR-14805 Import a repository from bitbucket cloud --- .../BitbucketCloudRestClient.java | 5 + .../ws/AlmIntegrationsWSModule.java | 2 + .../ImportBitbucketCloudRepoAction.java | 150 ++++++++++++ .../ImportBitbucketCloudRepoActionTest.java | 224 ++++++++++++++++++ 4 files changed, 381 insertions(+) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoAction.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoActionTest.java diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java index 1627f75869c..ca24928fca5 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java @@ -140,6 +140,11 @@ public class BitbucketCloudRestClient { return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class)); } + public Repository getRepo(String encodedCredentials, String workspace, String slug) { + HttpUrl url = buildUrl(String.format("/repositories/%s/%s", workspace, slug)); + return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class)); + } + public String createAccessToken(String clientId, String clientSecret) { Request request = createAccessTokenRequest(clientId, clientSecret); return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class)).getAccessToken(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java index 776327931cb..528b78e98bd 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java @@ -23,6 +23,7 @@ import org.sonar.core.platform.Module; import org.sonar.server.almintegration.ws.azure.ImportAzureProjectAction; import org.sonar.server.almintegration.ws.azure.ListAzureProjectsAction; import org.sonar.server.almintegration.ws.azure.SearchAzureReposAction; +import org.sonar.server.almintegration.ws.bitbucketcloud.ImportBitbucketCloudRepoAction; import org.sonar.server.almintegration.ws.bitbucketcloud.SearchBitbucketCloudReposAction; import org.sonar.server.almintegration.ws.bitbucketserver.ImportBitbucketServerProjectAction; import org.sonar.server.almintegration.ws.bitbucketserver.ListBitbucketServerProjectsAction; @@ -41,6 +42,7 @@ public class AlmIntegrationsWSModule extends Module { CheckPatAction.class, SetPatAction.class, ImportBitbucketServerProjectAction.class, + ImportBitbucketCloudRepoAction.class, ListBitbucketServerProjectsAction.class, SearchBitbucketServerReposAction.class, SearchBitbucketCloudReposAction.class, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoAction.java new file mode 100644 index 00000000000..eb79ccc3661 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoAction.java @@ -0,0 +1,150 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.almintegration.ws.bitbucketcloud; + +import javax.annotation.Nullable; +import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Repository; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +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.component.ComponentDto; +import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; +import org.sonar.server.almintegration.ws.ImportHelper; +import org.sonar.server.component.ComponentUpdater; +import org.sonar.server.component.NewComponent; +import org.sonar.server.project.ProjectDefaultVisibility; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Projects; + +import static java.util.Optional.ofNullable; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING; +import static org.sonar.server.almintegration.ws.ImportHelper.toCreateResponse; +import static org.sonar.server.component.NewComponent.newComponentBuilder; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class ImportBitbucketCloudRepoAction implements AlmIntegrationsWsAction { + + private static final String PARAM_REPO_SLUG = "repositorySlug"; + + private final DbClient dbClient; + private final UserSession userSession; + private final BitbucketCloudRestClient bitbucketCloudRestClient; + private final ProjectDefaultVisibility projectDefaultVisibility; + private final ComponentUpdater componentUpdater; + private final ImportHelper importHelper; + + public ImportBitbucketCloudRepoAction(DbClient dbClient, UserSession userSession, BitbucketCloudRestClient bitbucketCloudRestClient, + ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater, ImportHelper importHelper) { + this.dbClient = dbClient; + this.userSession = userSession; + this.bitbucketCloudRestClient = bitbucketCloudRestClient; + this.projectDefaultVisibility = projectDefaultVisibility; + this.componentUpdater = componentUpdater; + this.importHelper = importHelper; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("import_bitbucketcloud_repo") + .setDescription("Create a SonarQube project with the information from the provided Bitbucket Cloud repository.
" + + "Autoconfigure pull request decoration mechanism.
" + + "Requires the 'Create Projects' permission") + .setPost(true) + .setInternal(true) + .setSince("9.0") + .setHandler(this); + + action.createParam(PARAM_REPO_SLUG) + .setRequired(true) + .setMaximumLength(200) + .setDescription("Bitbucket Cloud repository slug"); + + action.createParam(PARAM_ALM_SETTING) + .setRequired(true) + .setMaximumLength(200) + .setDescription("ALM setting key"); + + } + + @Override + public void handle(Request request, Response response) { + Projects.CreateWsResponse createResponse = doHandle(request); + writeProtobuf(createResponse, request, response); + } + + private Projects.CreateWsResponse doHandle(Request request) { + importHelper.checkProvisionProjectPermission(); + + String userUuid = importHelper.getUserUuid(); + String repoSlug = request.mandatoryParam(PARAM_REPO_SLUG); + AlmSettingDto almSettingDto = importHelper.getAlmSetting(request); + String workspace = ofNullable(almSettingDto.getAppId()) + .orElseThrow(() -> new IllegalArgumentException(String.format("workspace for alm setting %s is missing", almSettingDto.getKey()))); + + try (DbSession dbSession = dbClient.openSession(false)) { + String pat = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto) + .map(AlmPatDto::getPersonalAccessToken) + .orElseThrow(() -> new IllegalArgumentException(String.format("Username and App Password for '%s' is missing", almSettingDto.getKey()))); + + Repository repo = bitbucketCloudRestClient.getRepo(pat, workspace, repoSlug); + + ComponentDto componentDto = createProject(dbSession, workspace, repo, null); + + populatePRSetting(dbSession, repo, componentDto, almSettingDto); + + componentUpdater.commitAndIndex(dbSession, componentDto); + + return toCreateResponse(componentDto); + } + } + + private ComponentDto createProject(DbSession dbSession, String workspace, Repository repo, @Nullable String defaultBranchName) { + boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate(); + NewComponent newProject = newComponentBuilder() + .setKey(workspace + "_" + repo.getSlug()) + .setName(repo.getName()) + .setPrivate(visibility) + .setQualifier(PROJECT) + .build(); + String userUuid = userSession.isLoggedIn() ? userSession.getUuid() : null; + + return componentUpdater.createWithoutCommit(dbSession, newProject, userUuid, defaultBranchName, p -> { + }); + } + + private void populatePRSetting(DbSession dbSession, Repository repo, ComponentDto componentDto, AlmSettingDto almSettingDto) { + ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto() + .setAlmSettingUuid(almSettingDto.getUuid()) + // PR decoration reads almRepo + .setAlmRepo(repo.getSlug()) + .setAlmSlug(repo.getSlug()) + .setProjectUuid(componentDto.uuid()) + .setMonorepo(false); + dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoActionTest.java new file mode 100644 index 00000000000..a2c526827b8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoActionTest.java @@ -0,0 +1,224 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.almintegration.ws.bitbucketcloud; + +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Project; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Repository; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.core.i18n.I18n; +import org.sonar.core.util.SequenceUuidFactory; +import org.sonar.db.DbTester; +import org.sonar.db.alm.pat.AlmPatDto; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.almintegration.ws.ImportHelper; +import org.sonar.server.component.ComponentUpdater; +import org.sonar.server.es.TestProjectIndexers; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.favorite.FavoriteUpdater; +import org.sonar.server.permission.PermissionTemplateService; +import org.sonar.server.project.ProjectDefaultVisibility; +import org.sonar.server.project.Visibility; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Projects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; +import static org.sonar.db.permission.GlobalPermission.SCAN; + +public class ImportBitbucketCloudRepoActionTest { + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class); + private final BitbucketCloudRestClient bitbucketCloudRestClient = mock(BitbucketCloudRestClient.class); + + private final ComponentUpdater componentUpdater = new ComponentUpdater(db.getDbClient(), mock(I18n.class), System2.INSTANCE, + mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory()); + + private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession); + private final WsActionTester ws = new WsActionTester(new ImportBitbucketCloudRepoAction(db.getDbClient(), userSession, + bitbucketCloudRestClient, projectDefaultVisibility, componentUpdater, importHelper)); + + @Before + public void before() { + when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE); + } + + @Test + public void import_project() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketCloudAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + }); + Repository repo = getGsonBBCRepo(); + when(bitbucketCloudRestClient.getRepo(any(), any(), any())).thenReturn(repo); + + Projects.CreateWsResponse response = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("repositorySlug", "repo-slug-1") + .executeProtobuf(Projects.CreateWsResponse.class); + + Projects.CreateWsResponse.Project result = response.getProject(); + assertThat(result.getKey()).isEqualTo(almSetting.getAppId() + "_" + repo.getSlug()); + assertThat(result.getName()).isEqualTo(repo.getName()); + + Optional projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); + assertThat(projectDto).isPresent(); + assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); + } + + @Test + public void fail_project_already_exist() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + }); + Repository repo = getGsonBBCRepo(); + String projectKey = almSetting.getAppId() + "_" + repo.getSlug(); + db.components().insertPublicProject(p -> p.setDbKey(projectKey)); + + when(bitbucketCloudRestClient.getRepo(any(), any(), any())).thenReturn(repo); + + TestRequest request = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("repositorySlug", "repo-slug-1"); + + assertThatThrownBy(request::execute) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Could not create null, key already exists: " + projectKey); + } + + @Test + public void fail_when_not_logged_in() { + TestRequest request = ws.newRequest() + .setParam("almSetting", "sdgfdshfjztutz") + .setParam("projectKey", "projectKey") + .setParam("repositorySlug", "repo-slug"); + + assertThatThrownBy(request::execute) + .isInstanceOf(UnauthorizedException.class); + } + + @Test + public void fail_when_missing_project_creator_permission() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(SCAN); + + TestRequest request = ws.newRequest() + .setParam("almSetting", "sdgfdshfjztutz") + .setParam("projectKey", "projectKey") + .setParam("repositorySlug", "repo-slug"); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Insufficient privileges"); + } + + @Test + public void check_pat_is_missing() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting(); + + TestRequest request = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("repositorySlug", "repo"); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Username and App Password for '" + almSetting.getKey() + "' is missing"); + } + + @Test + public void fail_check_alm_setting_not_found() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmPatDto almPatDto = newAlmPatDto(); + db.getDbClient().almPatDao().insert(db.getSession(), almPatDto); + + TestRequest request = ws.newRequest() + .setParam("almSetting", "testKey") + .setParam("repositorySlug", "repo"); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("ALM Setting 'testKey' not found"); + } + + @Test + public void fail_when_no_creation_project_permission() { + UserDto user = db.users().insertUser(); + userSession.logIn(user); + + TestRequest request = ws.newRequest() + .setParam("almSetting", "anyvalue"); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Insufficient privileges"); + } + + @Test + public void definition() { + WebService.Action def = ws.getDef(); + + assertThat(def.since()).isEqualTo("9.0"); + assertThat(def.isPost()).isTrue(); + assertThat(def.params()) + .extracting(WebService.Param::key, WebService.Param::isRequired) + .containsExactlyInAnyOrder( + tuple("almSetting", true), + tuple("repositorySlug", true)); + } + + private Repository getGsonBBCRepo() { + Project project1 = new Project("PROJECT-UUID-ONE", "projectKey1", "projectName1"); + Repository repo1 = new Repository("REPO-UUID-ONE", "repo-slug-1", "repoName1", project1); + return repo1; + } + +} -- 2.39.5