]> source.dussan.org Git - sonarqube.git/blob
f84d1e6b4585034266357393dc8a28d52c912b24
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 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.sonar.api.utils.log.Logger;
42 import org.sonar.api.utils.log.Loggers;
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 = Loggers.get(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   private static final String UNABLE_TO_CONTACT_BBC_SERVERS = "Unable to contact Bitbucket Cloud servers";
56   private static final String ERROR_BBC_SERVERS = "Error returned by Bitbucket Cloud";
57
58   protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
59
60   private final OkHttpClient client;
61   private final String bitbucketCloudEndpoint;
62   private final String accessTokenEndpoint;
63
64   @Inject
65   public BitbucketCloudRestClient(OkHttpClient okHttpClient) {
66     this(okHttpClient, ENDPOINT, ACCESS_TOKEN_ENDPOINT);
67   }
68
69   protected BitbucketCloudRestClient(OkHttpClient okHttpClient, String bitbucketCloudEndpoint, String accessTokenEndpoint) {
70     this.client = okHttpClient;
71     this.bitbucketCloudEndpoint = bitbucketCloudEndpoint;
72     this.accessTokenEndpoint = accessTokenEndpoint;
73   }
74
75   /**
76    * Validate parameters provided.
77    */
78   public void validate(String clientId, String clientSecret, String workspace) {
79     Token token = validateAccessToken(clientId, clientSecret);
80
81     if (token.getScopes() == null || !token.getScopes().contains("pullrequest")) {
82       String msg = "The OAuth consumer in the Bitbucket workspace is not configured with the permission to read pull requests";
83       LOG.info("Validation failed. {}}: {}", msg, token.getScopes());
84       throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + msg);
85     }
86
87     try {
88       doGet(token.getAccessToken(), buildUrl("/repositories/" + workspace), r -> null);
89     } catch (NotFoundException | IllegalStateException e) {
90       throw new IllegalArgumentException(e.getMessage());
91     }
92   }
93
94   /**
95    * Validate parameters provided.
96    */
97   public void validateAppPassword(String encodedCredentials, String workspace) {
98     try {
99       doGetWithBasicAuth(encodedCredentials, buildUrl("/repositories/" + workspace), r -> null);
100     } catch (NotFoundException | IllegalStateException e) {
101       throw new IllegalArgumentException(e.getMessage());
102     }
103   }
104
105   private Token validateAccessToken(String clientId, String clientSecret) {
106     Request request = createAccessTokenRequest(clientId, clientSecret);
107     try (Response response = client.newCall(request).execute()) {
108       if (response.isSuccessful()) {
109         return buildGson().fromJson(response.body().charStream(), Token.class);
110       }
111
112       ErrorDetails errorMsg = getTokenError(response.body());
113       if (errorMsg.body != null) {
114         switch (errorMsg.body) {
115           case "invalid_grant":
116             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS +
117               ": Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
118           case "unauthorized_client":
119             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Check your credentials");
120           default:
121             if (errorMsg.parsedErrorMsg != null) {
122               LOG.info("Validation failed: " + errorMsg.parsedErrorMsg);
123               throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + errorMsg.parsedErrorMsg);
124             } else {
125               LOG.info("Validation failed: " + errorMsg.body);
126               throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
127             }
128         }
129       } else {
130         LOG.info("Validation failed");
131       }
132       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
133
134     } catch (IOException e) {
135       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
136     }
137   }
138
139   public RepositoryList searchRepos(String encodedCredentials, String workspace, @Nullable String repoName, Integer page, Integer pageSize) {
140     String filterQuery = String.format("q=name~\"%s\"", repoName != null ? repoName : "");
141     HttpUrl url = buildUrl(String.format("/repositories/%s?%s&page=%s&pagelen=%s", workspace, filterQuery, page, pageSize));
142     return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
143   }
144
145   public Repository getRepo(String encodedCredentials, String workspace, String slug) {
146     HttpUrl url = buildUrl(String.format("/repositories/%s/%s", workspace, slug));
147     return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class));
148   }
149
150   public String createAccessToken(String clientId, String clientSecret) {
151     Request request = createAccessTokenRequest(clientId, clientSecret);
152     return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class)).getAccessToken();
153   }
154
155   private Request createAccessTokenRequest(String clientId, String clientSecret) {
156     RequestBody body = new FormBody.Builder()
157       .add("grant_type", "client_credentials")
158       .build();
159     HttpUrl url = HttpUrl.parse(accessTokenEndpoint);
160     String credential = Credentials.basic(clientId, clientSecret);
161     return prepareRequestWithBasicAuthCredentials(credential, "POST", url, body);
162   }
163
164   protected HttpUrl buildUrl(String relativeUrl) {
165     return HttpUrl.parse(removeEnd(bitbucketCloudEndpoint, "/") + "/" + VERSION + relativeUrl);
166   }
167
168   protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler) {
169     Request request = prepareRequestWithAccessToken(accessToken, GET, url, null);
170     return doCall(request, handler);
171   }
172
173   protected <G> G doGetWithBasicAuth(String encodedCredentials, HttpUrl url, Function<Response, G> handler) {
174     Request request = prepareRequestWithBasicAuthCredentials("Basic " + encodedCredentials, GET, url, null);
175     return doCall(request, handler);
176   }
177
178   protected <G> G doCall(Request request, Function<Response, G> handler) {
179     try (Response response = client.newCall(request).execute()) {
180       if (!response.isSuccessful()) {
181         handleError(response);
182       }
183       return handler.apply(response);
184     } catch (IOException e) {
185       throw new IllegalStateException(ERROR_BBC_SERVERS, e);
186     }
187   }
188
189   private static void handleError(Response response) throws IOException {
190     int responseCode = response.code();
191     ErrorDetails error = getError(response.body());
192     LOG.info(ERROR_BBC_SERVERS + ": {} {}", responseCode, error.parsedErrorMsg != null ? error.parsedErrorMsg : error.body);
193
194     if (error.parsedErrorMsg != null) {
195       throw new IllegalStateException(ERROR_BBC_SERVERS + ": " + error.parsedErrorMsg);
196     } else {
197       throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS);
198     }
199   }
200
201   private static ErrorDetails getError(@Nullable ResponseBody body) throws IOException {
202     return getErrorDetails(body, s -> {
203       Error gsonError = buildGson().fromJson(s, Error.class);
204       if (gsonError != null && gsonError.errorMsg != null && gsonError.errorMsg.message != null) {
205         return gsonError.errorMsg.message;
206       }
207       return null;
208     });
209   }
210
211   private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException {
212     if (body == null) {
213       return new ErrorDetails(null, null);
214     }
215     String bodyStr = body.string();
216     if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
217       try {
218         TokenError gsonError = buildGson().fromJson(bodyStr, TokenError.class);
219         if (gsonError != null && gsonError.error != null) {
220           return new ErrorDetails(gsonError.error, gsonError.errorDescription);
221         }
222       } catch (JsonParseException e) {
223         // ignore
224       }
225     }
226
227     return new ErrorDetails(bodyStr, null);
228   }
229
230   private static class ErrorDetails {
231     @Nullable
232     private final String body;
233     @Nullable
234     private final String parsedErrorMsg;
235
236     public ErrorDetails(@Nullable String body, @Nullable String parsedErrorMsg) {
237       this.body = body;
238       this.parsedErrorMsg = parsedErrorMsg;
239     }
240   }
241
242   private static ErrorDetails getErrorDetails(@Nullable ResponseBody body, UnaryOperator<String> parser) throws IOException {
243     if (body == null) {
244       return new ErrorDetails("", null);
245     }
246     String bodyStr = body.string();
247     if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
248       try {
249         return new ErrorDetails(bodyStr, parser.apply(bodyStr));
250       } catch (JsonParseException e) {
251         // ignore
252       }
253     }
254     return new ErrorDetails(bodyStr, null);
255   }
256
257   protected static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) {
258     return new Request.Builder()
259       .method(method, body)
260       .url(url)
261       .header(AUTHORIZATION, "Bearer " + accessToken)
262       .build();
263   }
264
265   protected static Request prepareRequestWithBasicAuthCredentials(String encodedCredentials, String method,
266     HttpUrl url, @Nullable RequestBody body) {
267     return new Request.Builder()
268       .method(method, body)
269       .url(url)
270       .header(AUTHORIZATION, encodedCredentials)
271       .build();
272   }
273
274   public static Gson buildGson() {
275     return new GsonBuilder().create();
276   }
277 }