Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Bitbucket operations to prevent leaking scope #574

Merged
merged 1 commit into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory;
Expand Down Expand Up @@ -90,6 +91,7 @@ public void load(CoreExtension.Context context) {
DefaultGithubClientFactory.class,
DefaultLinkHeaderReader.class,
RestApplicationAuthenticationProvider.class,
HttpClientBuilderFactory.class,
DefaultBitbucketClientFactory.class,
BitbucketValidator.class,
GitlabValidator.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -23,10 +23,8 @@
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;

import java.io.IOException;
import java.time.Instant;
Expand Down Expand Up @@ -55,21 +53,21 @@ public interface BitbucketClient {
*/
CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
String reportDescription, Instant creationDate, String dashboardUrl,
String logoUrl, QualityGate.Status status);
String logoUrl, ReportStatus reportStatus);

/**
* Deletes all code insights annotations for the given parameters.
*
* @throws IOException if the annotations cannot be deleted
*/
void deleteAnnotations(String project, String repo, String commitSha) throws IOException;
void deleteAnnotations(String commitSha) throws IOException;

/**
* Uploads CodeInsights Annotations for the given commit.
*
* @throws IOException if the annotations cannot be uploaded
*/
void uploadAnnotations(String project, String repo, String commitSha, Set<CodeInsightsAnnotation> annotations) throws IOException;
void uploadAnnotations(String commitSha, Set<CodeInsightsAnnotation> annotations) throws IOException;

/**
* Creates a DataValue of type DataValue.Link or DataValue.CloudLink depending on the implementation
Expand All @@ -79,7 +77,7 @@ CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
/**
* Uploads the code insights report for the given commit
*/
void uploadReport(String project, String repo, String commitSha, CodeInsightsReport codeInsightReport) throws IOException;
void uploadReport(String commitSha, CodeInsightsReport codeInsightReport) throws IOException;

/**
* <p>
Expand All @@ -104,32 +102,10 @@ CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
*/
AnnotationUploadLimit getAnnotationUploadLimit();

/**
* Extract the name of the project from the relevant configuration. The project is
* the value that should be used in the calls that take a `project` parameter.
*
* @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM
* @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project
* @return the resolved project name.
*/
String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto);

/**
* Extract the name of the repository from the relevant configuration. The project is
* the value that should be used in the calls that take a `repository` parameter.
*
* @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM
* @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project
* @return the resolved repository name.
*/
String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto);

/**
* Retrieve the details of the repository from the target Bitbucket instance.
* @param project the project as resolved from {@link #resolveProject(AlmSettingDto, ProjectAlmSettingDto)}
* @param repo the repository as resolved from {@link #resolveRepository(AlmSettingDto, ProjectAlmSettingDto)}
* @return the repository details retrieved from Bitbucket.
*/
Repository retrieveRepository(String project, String repo) throws IOException;
Repository retrieveRepository() throws IOException;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -21,24 +21,22 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.BitbucketCloudConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudCreateReportRequest;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -52,7 +50,6 @@

import static java.lang.String.format;


class BitbucketCloudClient implements BitbucketClient {

private static final Logger LOGGER = Loggers.get(BitbucketCloudClient.class);
Expand All @@ -64,27 +61,25 @@ class BitbucketCloudClient implements BitbucketClient {

private final ObjectMapper objectMapper;
private final OkHttpClient okHttpClient;
private final BitbucketConfiguration bitbucketConfiguration;

BitbucketCloudClient(BitbucketCloudConfiguration config, ObjectMapper objectMapper, OkHttpClient.Builder baseClientBuilder) {
this(objectMapper, createAuthorisingClient(baseClientBuilder, negotiateBearerToken(config, objectMapper, baseClientBuilder.build())));
}

BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient) {
BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient, BitbucketConfiguration bitbucketConfiguration) {
this.objectMapper = objectMapper;
this.okHttpClient = okHttpClient;
this.bitbucketConfiguration = bitbucketConfiguration;
}

private static String negotiateBearerToken(BitbucketCloudConfiguration bitbucketCloudConfiguration, ObjectMapper objectMapper, OkHttpClient okHttpClient) {
static String negotiateBearerToken(String clientId, String clientSecret, ObjectMapper objectMapper, OkHttpClient okHttpClient) {
Request request = new Request.Builder()
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((bitbucketCloudConfiguration.getClientId() + ":" + bitbucketCloudConfiguration.getSecret()).getBytes(
StandardCharsets.UTF_8)))
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8)))
.url("https://bitbucket.org/site/oauth2/access_token")
.post(RequestBody.create("grant_type=client_credentials", MediaType.parse("application/x-www-form-urlencoded")))
.build();

try (Response response = okHttpClient.newCall(request).execute()) {
AuthToken authToken = objectMapper.readValue(
Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), AuthToken.class);
BitbucketCloudClient.AuthToken authToken = objectMapper.readValue(
Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), BitbucketCloudClient.AuthToken.class);
return authToken.getAccessToken();
} catch (IOException ex) {
throw new IllegalStateException("Could not retrieve bearer token", ex);
Expand All @@ -106,7 +101,7 @@ public CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int
@Override
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription,
Instant creationDate, String dashboardUrl, String logoUrl,
QualityGate.Status status) {
ReportStatus status) {
return new CloudCreateReportRequest(
reportData,
reportDescription,
Expand All @@ -116,16 +111,17 @@ public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
dashboardUrl, // you need to change this to a real https URL for local debugging since localhost will get declined by the API
logoUrl,
"COVERAGE",
QualityGate.Status.ERROR.equals(status) ? "FAILED" : "PASSED"
ReportStatus.FAILED == status ? "FAILED" : "PASSED"
);
}

@Override
public void deleteAnnotations(String project, String repo, String commitSha) {
public void deleteAnnotations(String commitSha) {
// not needed here.
}

public void uploadAnnotations(String project, String repository, String commit, Set<CodeInsightsAnnotation> baseAnnotations) throws IOException {
@Override
public void uploadAnnotations(String commit, Set<CodeInsightsAnnotation> baseAnnotations) throws IOException {
Set<CloudAnnotation> annotations = baseAnnotations.stream().map(CloudAnnotation.class::cast).collect(Collectors.toSet());

if (annotations.isEmpty()) {
Expand All @@ -134,7 +130,7 @@ public void uploadAnnotations(String project, String repository, String commit,

Request req = new Request.Builder()
.post(RequestBody.create(objectMapper.writeValueAsString(annotations), APPLICATION_JSON_MEDIA_TYPE))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", project, repository, commit, REPORT_KEY))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY))
.build();

LOGGER.info("Creating annotations on bitbucket cloud");
Expand All @@ -151,10 +147,10 @@ public DataValue createLinkDataValue(String dashboardUrl) {
}

@Override
public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException {
deleteExistingReport(project, repository, commit);
public void uploadReport(String commit, CodeInsightsReport codeInsightReport) throws IOException {
deleteExistingReport(commit);

String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY);
String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY);
String body = objectMapper.writeValueAsString(codeInsightReport);
Request req = new Request.Builder()
.put(RequestBody.create(body, APPLICATION_JSON_MEDIA_TYPE))
Expand All @@ -180,20 +176,10 @@ public AnnotationUploadLimit getAnnotationUploadLimit() {
}

@Override
public String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return almSettingDto.getAppId();
}

@Override
public String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return projectAlmSettingDto.getAlmRepo();
}

@Override
public Repository retrieveRepository(String project, String repo) throws IOException {
public Repository retrieveRepository() throws IOException {
Request req = new Request.Builder()
.get()
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", project, repo))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository()))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
Expand All @@ -205,10 +191,10 @@ public Repository retrieveRepository(String project, String repo) throws IOExcep
}
}

void deleteExistingReport(String project, String repository, String commit) throws IOException {
void deleteExistingReport(String commit) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY))
.build();

LOGGER.info("Deleting existing reports on bitbucket cloud");
Expand All @@ -218,17 +204,6 @@ void deleteExistingReport(String project, String repository, String commit) thro
}
}

private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder baseClientBuilder, String bearerToken) {
return baseClientBuilder.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Bearer %s", bearerToken))
.addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build();
return chain.proceed(newRequest);
})
.build();
}

void validate(Response response) {
if (!response.isSuccessful()) {
String error = Optional.ofNullable(response.body()).map(b -> {
Expand All @@ -242,7 +217,7 @@ void validate(Response response) {
}
}

static class AuthToken {
private static class AuthToken {

private final String accessToken;

Expand Down
Loading