]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14371 Move 'check_pat', 'set_pat' WS to CE
authorJacek <jacek.poreda@sonarsource.com>
Thu, 28 Jan 2021 15:56:25 +0000 (16:56 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 4 Feb 2021 20:07:07 +0000 (20:07 +0000)
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java
server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java
server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java
server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/SetPatAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/CheckPatActionTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/SetPatActionTest.java [new file with mode: 0644]

index c4f2f4f0c574a2cf4a5dd642068d46e91c7a821c..36cb08dea3b1bd013204dd1085a5685dfd909eab 100644 (file)
@@ -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> G doGet(String token, String url, Function<Response, G> 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> G doGet(String token, String url, Function<Response, G> handler) {
     Request request = prepareRequestWithToken(token, GET, url, null);
     return doCall(request, handler);
   }
 
-  protected  <G> G doCall(Request request, Function<Response, G> handler) {
+  protected <G> G doCall(Request request, Function<Response, G> handler) {
     try (Response response = client.newCall(request).execute()) {
       checkResponseIsSuccessful(response);
       return handler.apply(response);
index 758db6e799f1008b3eb8acb6b57f5dfb462f4354..7a3ba3b63034d83fb1f75a92ed219c9ad638f7e6 100644 (file)
@@ -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));
index c296f79b5af9da093ba057620fbb4253a94dde87..40d3407943f0d2d1c181e2f9f177916ab99e9e1e 100644 (file)
@@ -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" +
index aac841beb37732dbcc15706ea918e155e470e819..a4cafcf1941a0b05992dc3c15bcff420d89dbe13 100644 (file)
@@ -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()
index fccfba4a7d2af73f34990361802b9340898f5279..01e1fa92172f1e1dbc3e5f296b3572a11f9a0008 100644 (file)
@@ -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 (file)
index 0000000..85e7f31
--- /dev/null
@@ -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<br/>" +
+        "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 (file)
index 0000000..d040049
--- /dev/null
@@ -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<br/>" +
+        "Only valid for Azure DevOps, Bitbucket Server & GitLab Alm Setting<br/>" +
+        "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> 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 (file)
index 0000000..5b09e6a
--- /dev/null
@@ -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 (file)
index 0000000..19aac89
--- /dev/null
@@ -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<AlmPatDto> 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<AlmPatDto> 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<AlmPatDto> 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<AlmPatDto> 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));
+  }
+
+}