From 05e6a0b265c7e79f168ab46437436f4d5a95ce82 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 28 Jan 2021 16:56:25 +0100 Subject: [PATCH] SONAR-14371 Move 'check_pat', 'set_pat' WS to CE --- .../client/azure/AzureDevOpsHttpClient.java | 26 +- .../BitbucketServerRestClient.java | 5 + .../azure/AzureDevOpsHttpClientTest.java | 52 +++- .../BitbucketServerRestClientTest.java | 40 ++++ .../ws/AlmIntegrationsWSModule.java | 2 + .../almintegration/ws/CheckPatAction.java | 121 ++++++++++ .../almintegration/ws/SetPatAction.java | 110 +++++++++ .../almintegration/ws/CheckPatActionTest.java | 226 ++++++++++++++++++ .../almintegration/ws/SetPatActionTest.java | 183 ++++++++++++++ 9 files changed, 757 insertions(+), 8 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/SetPatAction.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/CheckPatActionTest.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/SetPatActionTest.java diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java index c4f2f4f0c57..36cb08dea3b 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java @@ -23,13 +23,11 @@ import com.google.common.base.Strings; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; - import java.io.IOException; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.function.Function; import javax.annotation.Nullable; - import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; @@ -61,13 +59,18 @@ public class AzureDevOpsHttpClient { .build(); } + public void checkPAT(String serverUrl, String token) { + String url = String.format("%s/_apis/projects", getTrimmedUrl(serverUrl)); + LOG.debug(String.format("check pat : [%s]", url)); + doGet(token, url); + } + public GsonAzureProjectList getProjects(String serverUrl, String token) { String url = String.format("%s/_apis/projects", getTrimmedUrl(serverUrl)); LOG.debug(String.format("get projects : [%s]", url)); return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureProjectList.class)); } - public GsonAzureRepoList getRepos(String serverUrl, String token, @Nullable String projectName) { String url; if (projectName != null && !projectName.isEmpty()) { @@ -85,12 +88,25 @@ public class AzureDevOpsHttpClient { return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepo.class)); } - protected G doGet(String token, String url, Function handler) { + private void doGet(String token, String url) { + Request request = prepareRequestWithToken(token, GET, url, null); + doCall(request); + } + + protected void doCall(Request request) { + try (Response response = client.newCall(request).execute()) { + checkResponseIsSuccessful(response); + } catch (IOException e) { + throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER, e); + } + } + + protected G doGet(String token, String url, Function handler) { Request request = prepareRequestWithToken(token, GET, url, null); return doCall(request, handler); } - protected G doCall(Request request, Function handler) { + protected G doCall(Request request, Function handler) { try (Response response = client.newCall(request).execute()) { checkResponseIsSuccessful(response); return handler.apply(response); diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java index 758db6e799f..7a3ba3b6303 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java @@ -77,6 +77,11 @@ public class BitbucketServerRestClient { return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class)); } + public RepositoryList getRecentRepo(String serverUrl, String token) { + HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/profile/recent/repos"); + return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class)); + } + public ProjectList getProjects(String serverUrl, String token) { HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/projects"); return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), ProjectList.class)); diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java index c296f79b5af..40d3407943f 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java @@ -19,6 +19,9 @@ */ package org.sonar.alm.client.azure; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -34,9 +37,6 @@ import org.sonar.api.utils.log.LoggerLevel; 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 javax.annotation.Nullable; -import java.io.IOException; -import java.util.concurrent.TimeUnit; public class AzureDevOpsHttpClientTest { public static final String UNABLE_TO_CONTACT_AZURE = "Unable to contact Azure DevOps server, got an unexpected response"; @@ -60,6 +60,52 @@ public class AzureDevOpsHttpClientTest { server.shutdown(); } + @Test + public void check_pat() throws InterruptedException { + enqueueResponse(200, " { \"count\": 1,\n" + + " \"value\": [\n" + + " {\n" + + " \"id\": \"3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" + + " \"name\": \"Project\",\n" + + " \"description\": \"Project Description\",\n" + + " \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" + + " \"state\": \"wellFormed\",\n" + + " \"revision\": 63,\n" + + " \"visibility\": \"private\"\n" + + " }]}"); + + underTest.checkPAT(server.url("").toString(), "token"); + + RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS); + String azureDevOpsUrlCall = request.getRequestUrl().toString(); + assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "_apis/projects"); + assertThat(request.getMethod()).isEqualTo("GET"); + + assertThat(logTester.logs()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.DEBUG)) + .contains("check pat : [" + server.url("").toString() + "_apis/projects]"); + } + + @Test + public void check_invalid_pat() { + enqueueResponse(401); + + String serverUrl = server.url("").toString(); + assertThatThrownBy(() -> underTest.checkPAT(serverUrl, "invalid-token")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid personal access token"); + } + + @Test + public void check_pat_with_server_error() { + enqueueResponse(500); + + String serverUrl = server.url("").toString(); + assertThatThrownBy(() -> underTest.checkPAT(serverUrl, "token")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unable to contact Azure DevOps server"); + } + @Test public void get_projects() throws InterruptedException { enqueueResponse(200, " { \"count\": 2,\n" + diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java index aac841beb37..a4cafcf1941 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java @@ -87,6 +87,46 @@ public class BitbucketServerRestClientTest { tuple(1L, "potato", "potato", 1L, "HEY", "hey")); } + @Test + public void get_recent_repos() { + server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody("{\n" + + " \"isLastPage\": true,\n" + + " \"values\": [\n" + + " {\n" + + " \"slug\": \"banana\",\n" + + " \"id\": 2,\n" + + " \"name\": \"banana\",\n" + + " \"project\": {\n" + + " \"key\": \"HOY\",\n" + + " \"id\": 2,\n" + + " \"name\": \"hoy\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"slug\": \"potato\",\n" + + " \"id\": 1,\n" + + " \"name\": \"potato\",\n" + + " \"project\": {\n" + + " \"key\": \"HEY\",\n" + + " \"id\": 1,\n" + + " \"name\": \"hey\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}")); + + RepositoryList gsonBBSRepoList = underTest.getRecentRepo(server.url("/").toString(), "token"); + assertThat(gsonBBSRepoList.isLastPage()).isTrue(); + assertThat(gsonBBSRepoList.getValues()).hasSize(2); + assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug, + g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName()) + .containsExactlyInAnyOrder( + tuple(2L, "banana", "banana", 2L, "HOY", "hoy"), + tuple(1L, "potato", "potato", 1L, "HEY", "hey")); + } + @Test public void get_repo() { server.enqueue(new MockResponse() 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 fccfba4a7d2..01e1fa92172 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 @@ -37,6 +37,8 @@ public class AlmIntegrationsWSModule extends Module { @Override protected void configureModule() { add( + CheckPatAction.class, + SetPatAction.class, ImportBitbucketServerProjectAction.class, ListBitbucketServerProjectsAction.class, SearchBitbucketServerReposAction.class, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java new file mode 100644 index 00000000000..85e7f31bd02 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java @@ -0,0 +1,121 @@ +/* + * 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; + +import org.sonar.alm.client.azure.AzureDevOpsHttpClient; +import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient; +import org.sonar.alm.client.gitlab.GitlabHttpClient; +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.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; + +import static java.util.Objects.requireNonNull; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; + +public class CheckPatAction implements AlmIntegrationsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PAT_CANNOT_BE_NULL = "PAT cannot be null"; + private static final String URL_CANNOT_BE_NULL = "URL cannot be null"; + + private final DbClient dbClient; + private final UserSession userSession; + private final AzureDevOpsHttpClient azureDevOpsHttpClient; + private final BitbucketServerRestClient bitbucketServerRestClient; + private final GitlabHttpClient gitlabHttpClient; + + public CheckPatAction(DbClient dbClient, UserSession userSession, + AzureDevOpsHttpClient azureDevOpsHttpClient, + BitbucketServerRestClient bitbucketServerRestClient, + GitlabHttpClient gitlabHttpClient) { + this.dbClient = dbClient; + this.userSession = userSession; + this.azureDevOpsHttpClient = azureDevOpsHttpClient; + this.bitbucketServerRestClient = bitbucketServerRestClient; + this.gitlabHttpClient = gitlabHttpClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("check_pat") + .setDescription("Check validity of a Personal Access Token for the given ALM setting
" + + "Requires the 'Create Projects' permission") + .setPost(false) + .setInternal(true) + .setSince("8.2") + .setHandler(this); + + action.createParam(PARAM_ALM_SETTING) + .setRequired(true) + .setMaximumLength(200) + .setDescription("ALM setting key"); + } + + @Override + public void handle(Request request, Response response) { + doHandle(request); + response.noContent(); + } + + private void doHandle(Request request) { + try (DbSession dbSession = dbClient.openSession(false)) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); + + String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); + String userUuid = requireNonNull(userSession.getUuid(), "User cannot be null"); + AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) + .orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey))); + + AlmPatDto almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto) + .orElseThrow(() -> new IllegalArgumentException(String.format("personal access token for '%s' is missing", almSettingKey))); + + switch (almSettingDto.getAlm()) { + case AZURE_DEVOPS: + azureDevOpsHttpClient.checkPAT( + requireNonNull(almSettingDto.getUrl(), URL_CANNOT_BE_NULL), + requireNonNull(almPatDto.getPersonalAccessToken(), PAT_CANNOT_BE_NULL)); + break; + case BITBUCKET: + // Do an authenticate call to Bitbucket Server to validate that the user's personal access token is valid + bitbucketServerRestClient.getRecentRepo( + requireNonNull(almSettingDto.getUrl(), URL_CANNOT_BE_NULL), + requireNonNull(almPatDto.getPersonalAccessToken(), PAT_CANNOT_BE_NULL)); + break; + case GITLAB: + gitlabHttpClient.searchProjects( + requireNonNull(almSettingDto.getUrl(), URL_CANNOT_BE_NULL), + requireNonNull(almPatDto.getPersonalAccessToken(), PAT_CANNOT_BE_NULL), + null, 1, 10); + break; + case GITHUB: + default: + throw new IllegalArgumentException(String.format("unsupported ALM %s", almSettingDto.getAlm())); + } + + } + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/SetPatAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/SetPatAction.java new file mode 100644 index 00000000000..d040049a778 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/SetPatAction.java @@ -0,0 +1,110 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Optional; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.Preconditions; +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.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.sonar.db.alm.setting.ALM.AZURE_DEVOPS; +import static org.sonar.db.alm.setting.ALM.BITBUCKET; +import static org.sonar.db.alm.setting.ALM.GITLAB; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; + +public class SetPatAction implements AlmIntegrationsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PAT = "pat"; + + private final DbClient dbClient; + private final UserSession userSession; + + public SetPatAction(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_pat") + .setDescription("Set a Personal Access Token for the given ALM setting
" + + "Only valid for Azure DevOps, Bitbucket Server & GitLab Alm Setting
" + + "Requires the 'Create Projects' permission") + .setPost(true) + .setSince("8.2") + .setHandler(this); + + action.createParam(PARAM_ALM_SETTING) + .setRequired(true) + .setDescription("ALM setting key"); + action.createParam(PARAM_PAT) + .setRequired(true) + .setMaximumLength(2000) + .setDescription("Personal Access Token"); + } + + @Override + public void handle(Request request, Response response) { + doHandle(request); + response.noContent(); + } + + private void doHandle(Request request) { + try (DbSession dbSession = dbClient.openSession(false)) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); + + String pat = request.mandatoryParam(PARAM_PAT); + String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); + + String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null"); + AlmSettingDto almSetting = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) + .orElseThrow(() -> new NotFoundException(format("ALM Setting '%s' not found", almSettingKey))); + + Preconditions.checkArgument(Arrays.asList(AZURE_DEVOPS, BITBUCKET, GITLAB) + .contains(almSetting.getAlm()), "Only Azure DevOps, Bibucket Server and GitLab ALM Settings are supported."); + + Optional almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSetting); + if (almPatDto.isPresent()) { + AlmPatDto almPat = almPatDto.get(); + almPat.setPersonalAccessToken(pat); + dbClient.almPatDao().update(dbSession, almPat); + } else { + AlmPatDto almPat = new AlmPatDto() + .setPersonalAccessToken(pat) + .setAlmSettingUuid(almSetting.getUuid()) + .setUserUuid(userUuid); + dbClient.almPatDao().insert(dbSession, almPat); + } + dbSession.commit(); + } + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/CheckPatActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/CheckPatActionTest.java new file mode 100644 index 00000000000..5b09e6a4f9f --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/CheckPatActionTest.java @@ -0,0 +1,226 @@ +/* + * 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; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.alm.client.azure.AzureDevOpsHttpClient; +import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient; +import org.sonar.alm.client.gitlab.GitlabHttpClient; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbTester; +import org.sonar.db.alm.pat.AlmPatDto; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; + +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.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +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; + +public class CheckPatActionTest { + + public static final String PAT_SECRET = "pat-secret"; + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final AzureDevOpsHttpClient azureDevOpsPrHttpClient = mock(AzureDevOpsHttpClient.class); + private final BitbucketServerRestClient bitbucketServerRestClient = mock(BitbucketServerRestClient.class); + private final GitlabHttpClient gitlabPrHttpClient = mock(GitlabHttpClient.class); + private final WsActionTester ws = new WsActionTester(new CheckPatAction(db.getDbClient(), userSession, azureDevOpsPrHttpClient, bitbucketServerRestClient, gitlabPrHttpClient)); + + @Test + public void check_pat_for_github() { + 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()); + }); + + TestRequest request = ws.newRequest().setParam("almSetting", almSetting.getKey()); + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("unsupported ALM GITHUB"); + } + + @Test + public void check_pat_for_azure_devops() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + dto.setPersonalAccessToken(PAT_SECRET); + }); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .execute(); + + assertThat(almSetting.getUrl()).isNotNull(); + verify(azureDevOpsPrHttpClient).checkPAT(almSetting.getUrl(), PAT_SECRET); + } + + @Test + public void check_pat_for_bitbucket() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + dto.setPersonalAccessToken(PAT_SECRET); + }); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .execute(); + + assertThat(almSetting.getUrl()).isNotNull(); + verify(bitbucketServerRestClient).getRecentRepo(almSetting.getUrl(), PAT_SECRET); + } + + @Test + public void check_pat_for_gitlab() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + dto.setPersonalAccessToken(PAT_SECRET); + }); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .execute(); + + assertThat(almSetting.getUrl()).isNotNull(); + verify(gitlabPrHttpClient).searchProjects(almSetting.getUrl(), PAT_SECRET, null, 1, 10); + } + + @Test + public void fail_when_personal_access_token_is_invalid_for_bitbucket() { + when(bitbucketServerRestClient.getRecentRepo(any(), any())).thenThrow(new IllegalArgumentException("Invalid personal access token")); + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + }); + + TestRequest request = ws.newRequest().setParam("almSetting", almSetting.getKey()); + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid personal access token"); + } + + @Test + public void fail_when_personal_access_token_is_invalid_for_gitlab() { + when(gitlabPrHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt())) + .thenThrow(new IllegalArgumentException("Invalid personal access token")); + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + }); + + TestRequest request = ws.newRequest().setParam("almSetting", almSetting.getKey()); + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid personal access token"); + } + + @Test + public void fail_when_not_logged_in() { + TestRequest request = ws.newRequest().setParam("almSetting", "anyvalue"); + assertThatThrownBy(request::execute) + .isInstanceOf(UnauthorizedException.class); + } + + @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) + .hasMessage("Insufficient privileges"); + } + + @Test + public void check_pat_is_missing() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + + TestRequest request = ws.newRequest().setParam("almSetting", almSetting.getKey()); + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("personal access token for '" + almSetting.getKey() + "' is missing"); + } + + @Test + public void fail_check_pat_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"); + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("ALM Setting 'testKey' not found"); + } + + @Test + public void definition() { + WebService.Action def = ws.getDef(); + + assertThat(def.since()).isEqualTo("8.2"); + assertThat(def.isPost()).isFalse(); + assertThat(def.isInternal()).isTrue(); + assertThat(def.params()) + .extracting(WebService.Param::key, WebService.Param::isRequired) + .containsExactlyInAnyOrder(tuple("almSetting", true)); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/SetPatActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/SetPatActionTest.java new file mode 100644 index 00000000000..19aac89688b --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/SetPatActionTest.java @@ -0,0 +1,183 @@ +/* + * 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; + +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbTester; +import org.sonar.db.alm.pat.AlmPatDto; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; + +public class SetPatActionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final WsActionTester ws = new WsActionTester(new SetPatAction(db.getDbClient(), userSession)); + + @Test + public void set_new_azuredevops_pat() { + UserDto user = db.users().insertUser(); + AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("pat", "12345678987654321") + .execute(); + + Optional actualAlmPat = db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), user.getUuid(), almSetting); + assertThat(actualAlmPat).isPresent(); + assertThat(actualAlmPat.get().getPersonalAccessToken()).isEqualTo("12345678987654321"); + assertThat(actualAlmPat.get().getUserUuid()).isEqualTo(user.getUuid()); + assertThat(actualAlmPat.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid()); + } + + @Test + public void set_new_bitbucketserver_pat() { + UserDto user = db.users().insertUser(); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("pat", "12345678987654321") + .execute(); + + Optional actualAlmPat = db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), user.getUuid(), almSetting); + assertThat(actualAlmPat).isPresent(); + assertThat(actualAlmPat.get().getPersonalAccessToken()).isEqualTo("12345678987654321"); + assertThat(actualAlmPat.get().getUserUuid()).isEqualTo(user.getUuid()); + assertThat(actualAlmPat.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid()); + } + + @Test + public void set_new_gitlab_pat() { + UserDto user = db.users().insertUser(); + AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("pat", "12345678987654321") + .execute(); + + Optional actualAlmPat = db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), user.getUuid(), almSetting); + assertThat(actualAlmPat).isPresent(); + assertThat(actualAlmPat.get().getPersonalAccessToken()).isEqualTo("12345678987654321"); + assertThat(actualAlmPat.get().getUserUuid()).isEqualTo(user.getUuid()); + assertThat(actualAlmPat.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid()); + } + + @Test + public void set_existing_pat() { + UserDto user = db.users().insertUser(); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + db.almPats().insert(p -> p.setUserUuid(user.getUuid()), p -> p.setAlmSettingUuid(almSetting.getUuid())); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("pat", "newtoken") + .execute(); + + Optional actualAlmPat = db.getDbClient().almPatDao().selectByUserAndAlmSetting(db.getSession(), user.getUuid(), almSetting); + assertThat(actualAlmPat).isPresent(); + assertThat(actualAlmPat.get().getPersonalAccessToken()).isEqualTo("newtoken"); + assertThat(actualAlmPat.get().getUserUuid()).isEqualTo(user.getUuid()); + assertThat(actualAlmPat.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid()); + } + + @Test + public void fail_when_alm_setting_unknow() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("ALM Setting 'notExistingKey' not found"); + + ws.newRequest() + .setParam("almSetting", "notExistingKey") + .setParam("pat", "12345678987654321") + .execute(); + } + + @Test + public void fail_when_alm_setting_not_bitbucket_server_nor_gitlab() { + UserDto user = db.users().insertUser(); + AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Only Azure DevOps, Bibucket Server and GitLab ALM Settings are supported."); + + ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("pat", "12345678987654321") + .execute(); + } + + @Test + public void fail_when_not_logged_in() { + expectedException.expect(UnauthorizedException.class); + + ws.newRequest().execute(); + } + + @Test + public void fail_when_no_creation_project_permission() { + UserDto user = db.users().insertUser(); + userSession.logIn(user); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + ws.newRequest().execute(); + } + + @Test + public void definition() { + WebService.Action def = ws.getDef(); + + assertThat(def.since()).isEqualTo("8.2"); + assertThat(def.isPost()).isTrue(); + assertThat(def.params()) + .extracting(WebService.Param::key, WebService.Param::isRequired) + .containsExactlyInAnyOrder(tuple("almSetting", true), tuple("pat", true)); + } + +} -- 2.39.5