]> source.dussan.org Git - sonarqube.git/blob
79a99dccb1ba22d7d282ea2786f714e1327823f8
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.alm.client.bitbucket.bitbucketcloud;
21
22 import com.google.gson.Gson;
23 import com.google.gson.GsonBuilder;
24 import com.google.gson.JsonParseException;
25 import java.io.IOException;
26 import java.util.Objects;
27 import java.util.function.Function;
28 import java.util.function.UnaryOperator;
29 import javax.annotation.Nullable;
30 import javax.inject.Inject;
31 import okhttp3.Credentials;
32 import okhttp3.FormBody;
33 import okhttp3.HttpUrl;
34 import okhttp3.MediaType;
35 import okhttp3.OkHttpClient;
36 import okhttp3.Request;
37 import okhttp3.RequestBody;
38 import okhttp3.Response;
39 import okhttp3.ResponseBody;
40 import org.sonar.api.server.ServerSide;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.sonar.server.exceptions.NotFoundException;
44
45 import static org.sonar.api.internal.apachecommons.lang.StringUtils.removeEnd;
46
47 @ServerSide
48 public class BitbucketCloudRestClient {
49   private static final Logger LOG = LoggerFactory.getLogger(BitbucketCloudRestClient.class);
50   private static final String AUTHORIZATION = "Authorization";
51   private static final String GET = "GET";
52   private static final String ENDPOINT = "https://api.bitbucket.org";
53   private static final String ACCESS_TOKEN_ENDPOINT = "https://bitbucket.org/site/oauth2/access_token";
54   private static final String VERSION = "2.0";
55   protected static final String ERROR_BBC_SERVERS = "Error returned by Bitbucket Cloud";
56   protected static final String UNABLE_TO_CONTACT_BBC_SERVERS = "Unable to contact Bitbucket Cloud servers";
57   protected static final String MISSING_PULL_REQUEST_READ_PERMISSION = "The OAuth consumer in the Bitbucket workspace is not configured with the permission to read pull requests.";
58   protected static final String SCOPE = "Scope is: %s";
59   protected static final String UNAUTHORIZED_CLIENT = "Check your credentials";
60   protected static final String OAUTH_CONSUMER_NOT_PRIVATE = "Configure the OAuth consumer in the Bitbucket workspace to be a private consumer";
61   protected static final String BBC_FAIL_WITH_RESPONSE = "Bitbucket Cloud API call to [%s] failed with %s http code. Bitbucket Cloud response content : [%s]";
62   protected static final String BBC_FAIL_WITH_ERROR = "Bitbucket Cloud API call to [%s] failed with error: %s";
63
64   protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
65
66   private final OkHttpClient client;
67   private final String bitbucketCloudEndpoint;
68   private final String accessTokenEndpoint;
69
70
71   @Inject
72   public BitbucketCloudRestClient(OkHttpClient bitBucketCloudHttpClient) {
73     this(bitBucketCloudHttpClient, ENDPOINT, ACCESS_TOKEN_ENDPOINT);
74   }
75
76   protected BitbucketCloudRestClient(OkHttpClient bitBucketCloudHttpClient, String bitbucketCloudEndpoint, String accessTokenEndpoint) {
77     this.client = bitBucketCloudHttpClient;
78     this.bitbucketCloudEndpoint = bitbucketCloudEndpoint;
79     this.accessTokenEndpoint = accessTokenEndpoint;
80   }
81
82   /**
83    * Validate parameters provided.
84    */
85   public void validate(String clientId, String clientSecret, String workspace) {
86     Token token = validateAccessToken(clientId, clientSecret);
87
88     if (token.getScopes() == null || !token.getScopes().contains("pullrequest")) {
89       LOG.info(MISSING_PULL_REQUEST_READ_PERMISSION + String.format(SCOPE, token.getScopes()));
90       throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + MISSING_PULL_REQUEST_READ_PERMISSION);
91     }
92
93     try {
94       doGet(token.getAccessToken(), buildUrl("/repositories/" + workspace), r -> null);
95     } catch (NotFoundException | IllegalStateException e) {
96       throw new IllegalArgumentException(e.getMessage());
97     }
98   }
99
100   /**
101    * Validate parameters provided.
102    */
103   public void validateAppPassword(String encodedCredentials, String workspace) {
104     try {
105       doGetWithBasicAuth(encodedCredentials, buildUrl("/repositories/" + workspace), r -> null);
106     } catch (NotFoundException | IllegalStateException e) {
107       throw new IllegalArgumentException(e.getMessage());
108     }
109   }
110
111   private Token validateAccessToken(String clientId, String clientSecret) {
112     Request request = createAccessTokenRequest(clientId, clientSecret);
113     try (Response response = client.newCall(request).execute()) {
114       if (response.isSuccessful()) {
115         return buildGson().fromJson(response.body().charStream(), Token.class);
116       }
117
118       ErrorDetails errorMsg = getTokenError(response.body());
119       if (errorMsg.body != null) {
120         LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), errorMsg.body));
121         switch (errorMsg.body) {
122           case "invalid_grant":
123             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + OAUTH_CONSUMER_NOT_PRIVATE);
124           case "unauthorized_client":
125             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + UNAUTHORIZED_CLIENT);
126           default:
127             if (errorMsg.parsedErrorMsg != null) {
128               throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + errorMsg.parsedErrorMsg);
129             } else {
130               throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
131             }
132         }
133       } else {
134         LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), response.message()));
135       }
136       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
137
138     } catch (IOException e) {
139       LOG.info(String.format(BBC_FAIL_WITH_ERROR, request.url(), e.getMessage()));
140       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
141     }
142   }
143
144   public RepositoryList searchRepos(String encodedCredentials, String workspace, @Nullable String repoName, Integer page, Integer pageSize) {
145     String filterQuery = String.format("q=name~\"%s\"", repoName != null ? repoName : "");
146     HttpUrl url = buildUrl(String.format("/repositories/%s?%s&page=%s&pagelen=%s", workspace, filterQuery, page, pageSize));
147     return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
148   }
149
150   public Repository getRepo(String encodedCredentials, String workspace, String slug) {
151     HttpUrl url = buildUrl(String.format("/repositories/%s/%s", workspace, slug));
152     return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class));
153   }
154
155   public String createAccessToken(String clientId, String clientSecret) {
156     Request request = createAccessTokenRequest(clientId, clientSecret);
157     return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class)).getAccessToken();
158   }
159
160   private Request createAccessTokenRequest(String clientId, String clientSecret) {
161     RequestBody body = new FormBody.Builder()
162       .add("grant_type", "client_credentials")
163       .build();
164     HttpUrl url = HttpUrl.parse(accessTokenEndpoint);
165     String credential = Credentials.basic(clientId, clientSecret);
166     return prepareRequestWithBasicAuthCredentials(credential, "POST", url, body);
167   }
168
169   protected HttpUrl buildUrl(String relativeUrl) {
170     return HttpUrl.parse(removeEnd(bitbucketCloudEndpoint, "/") + "/" + VERSION + relativeUrl);
171   }
172
173   protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler) {
174     Request request = prepareRequestWithAccessToken(accessToken, GET, url, null);
175     return doCall(request, handler);
176   }
177
178   protected <G> G doGetWithBasicAuth(String encodedCredentials, HttpUrl url, Function<Response, G> handler) {
179     Request request = prepareRequestWithBasicAuthCredentials("Basic " + encodedCredentials, GET, url, null);
180     return doCall(request, handler);
181   }
182
183   protected <G> G doCall(Request request, Function<Response, G> handler) {
184     try (Response response = client.newCall(request).execute()) {
185       if (!response.isSuccessful()) {
186         handleError(response);
187       }
188       return handler.apply(response);
189     } catch (IOException e) {
190       LOG.info(ERROR_BBC_SERVERS + ": {}", e.getMessage());
191       throw new IllegalStateException(ERROR_BBC_SERVERS, e);
192     }
193   }
194
195   private static void handleError(Response response) throws IOException {
196     ErrorDetails error = getError(response.body());
197     LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), error.body));
198     if (error.parsedErrorMsg != null) {
199       throw new IllegalStateException(ERROR_BBC_SERVERS + ": " + error.parsedErrorMsg);
200     } else {
201       throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS);
202     }
203   }
204
205   private static ErrorDetails getError(@Nullable ResponseBody body) throws IOException {
206     return getErrorDetails(body, s -> {
207       Error gsonError = buildGson().fromJson(s, Error.class);
208       if (gsonError != null && gsonError.errorMsg != null && gsonError.errorMsg.message != null) {
209         return gsonError.errorMsg.message;
210       }
211       return null;
212     });
213   }
214
215   private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException {
216     if (body == null) {
217       return new ErrorDetails(null, null);
218     }
219     String bodyStr = body.string();
220     if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
221       try {
222         TokenError gsonError = buildGson().fromJson(bodyStr, TokenError.class);
223         if (gsonError != null && gsonError.error != null) {
224           return new ErrorDetails(gsonError.error, gsonError.errorDescription);
225         }
226       } catch (JsonParseException e) {
227         // ignore
228       }
229     }
230
231     return new ErrorDetails(bodyStr, null);
232   }
233
234   private static class ErrorDetails {
235     @Nullable
236     private final String body;
237     @Nullable
238     private final String parsedErrorMsg;
239
240     public ErrorDetails(@Nullable String body, @Nullable String parsedErrorMsg) {
241       this.body = body;
242       this.parsedErrorMsg = parsedErrorMsg;
243     }
244   }
245
246   private static ErrorDetails getErrorDetails(@Nullable ResponseBody body, UnaryOperator<String> parser) throws IOException {
247     if (body == null) {
248       return new ErrorDetails("", null);
249     }
250     String bodyStr = body.string();
251     if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
252       try {
253         return new ErrorDetails(bodyStr, parser.apply(bodyStr));
254       } catch (JsonParseException e) {
255         // ignore
256       }
257     }
258     return new ErrorDetails(bodyStr, null);
259   }
260
261   protected static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) {
262     return new Request.Builder()
263       .method(method, body)
264       .url(url)
265       .header(AUTHORIZATION, "Bearer " + accessToken)
266       .build();
267   }
268
269   protected static Request prepareRequestWithBasicAuthCredentials(String encodedCredentials, String method,
270     HttpUrl url, @Nullable RequestBody body) {
271     return new Request.Builder()
272       .method(method, body)
273       .url(url)
274       .header(AUTHORIZATION, encodedCredentials)
275       .build();
276   }
277
278   public static Gson buildGson() {
279     return new GsonBuilder().create();
280   }
281 }