]> source.dussan.org Git - sonarqube.git/blob
1f3cc008f6a8872bdd1bf7802cd314e37f9f94f4
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2021 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.net.HttpURLConnection;
27 import java.util.Objects;
28 import java.util.function.Function;
29 import java.util.function.UnaryOperator;
30 import javax.annotation.Nullable;
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.internal.apachecommons.io.IOUtils;
41 import org.sonar.api.server.ServerSide;
42 import org.sonar.api.utils.log.Logger;
43 import org.sonar.api.utils.log.Loggers;
44 import org.sonar.server.exceptions.NotFoundException;
45
46 import static org.sonar.api.internal.apachecommons.lang.StringUtils.removeEnd;
47
48 @ServerSide
49 public class BitbucketCloudRestClient {
50   private static final Logger LOG = Loggers.get(BitbucketCloudRestClient.class);
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   protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
57
58   private final OkHttpClient client;
59   private final String bitbucketCloudEndpoint;
60   private final String accessTokenEndpoint;
61
62   public BitbucketCloudRestClient(OkHttpClient okHttpClient) {
63     this(okHttpClient, ENDPOINT, ACCESS_TOKEN_ENDPOINT);
64   }
65
66   protected BitbucketCloudRestClient(OkHttpClient okHttpClient, String bitbucketCloudEndpoint, String accessTokenEndpoint) {
67     this.client = okHttpClient;
68     this.bitbucketCloudEndpoint = bitbucketCloudEndpoint;
69     this.accessTokenEndpoint = accessTokenEndpoint;
70   }
71
72   /**
73    * Validate parameters provided.
74    */
75   public void validate(String clientId, String clientSecret, String workspace) {
76     String accessToken = validateAccessToken(clientId, clientSecret);
77     doGet(accessToken, buildUrl("/workspaces/" + workspace + "/permissions"), r -> null, true);
78   }
79
80   private String validateAccessToken(String clientId, String clientSecret) {
81     Response response = null;
82     try {
83       Request request = createAccessTokenRequest(clientId, clientSecret);
84       response = client.newCall(request).execute();
85       if (response.isSuccessful()) {
86         return buildGson().fromJson(response.body().charStream(), Token.class).getAccessToken();
87       }
88
89       ErrorDetails errorMsg = getTokenError(response.body());
90       if (errorMsg.parsedErrorMsg != null) {
91         switch (errorMsg.parsedErrorMsg) {
92           case "invalid_grant":
93             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS +
94               ": Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
95           case "unauthorized_client":
96             throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Check your credentials");
97           default:
98             LOG.info("Validation failed: " + errorMsg.parsedErrorMsg);
99         }
100       } else {
101         LOG.info("Validation failed: " + errorMsg.body);
102       }
103       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
104
105     } catch (IOException e) {
106       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
107     } finally {
108       if (response != null && response.body() != null) {
109         IOUtils.closeQuietly(response);
110       }
111     }
112   }
113
114   public String createAccessToken(String clientId, String clientSecret) {
115     Request request = createAccessTokenRequest(clientId, clientSecret);
116     return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class), false).getAccessToken();
117   }
118
119   private Request createAccessTokenRequest(String clientId, String clientSecret) {
120     RequestBody body = new FormBody.Builder()
121       .add("grant_type", "client_credentials")
122       .build();
123     HttpUrl url = HttpUrl.parse(accessTokenEndpoint);
124     String credential = Credentials.basic(clientId, clientSecret);
125     return new Request.Builder()
126       .method("POST", body)
127       .url(url)
128       .header("Authorization", credential)
129       .build();
130   }
131
132   protected HttpUrl buildUrl(String relativeUrl) {
133     return HttpUrl.parse(removeEnd(bitbucketCloudEndpoint, "/") + "/" + VERSION + relativeUrl);
134   }
135
136   protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler, boolean throwErrorDetails) {
137     Request request = prepareRequestWithAccessToken(accessToken, GET, url, null);
138     return doCall(request, handler, throwErrorDetails);
139   }
140
141   protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler) {
142     return doGet(accessToken, url, handler, false);
143   }
144
145   protected void doPost(String accessToken, HttpUrl url, RequestBody body) {
146     Request request = prepareRequestWithAccessToken(accessToken, "POST", url, body);
147     doCall(request, r -> null, false);
148   }
149
150   protected void doPut(String accessToken, HttpUrl url, String json) {
151     RequestBody body = RequestBody.create(json, JSON_MEDIA_TYPE);
152     Request request = prepareRequestWithAccessToken(accessToken, "PUT", url, body);
153     doCall(request, r -> null, false);
154   }
155
156   protected void doDelete(String accessToken, HttpUrl url) {
157     Request request = prepareRequestWithAccessToken(accessToken, "DELETE", url, null);
158     doCall(request, r -> null, false);
159   }
160
161   private <G> G doCall(Request request, Function<Response, G> handler, boolean throwErrorDetails) {
162     try (Response response = client.newCall(request).execute()) {
163       if (!response.isSuccessful()) {
164         handleError(response, throwErrorDetails);
165       }
166       return handler.apply(response);
167     } catch (IOException e) {
168       throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
169     }
170   }
171
172   private static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) {
173     return new Request.Builder()
174       .method(method, body)
175       .url(url)
176       .header("Authorization", "Bearer " + accessToken)
177       .build();
178   }
179
180   public static Gson buildGson() {
181     return new GsonBuilder().create();
182   }
183
184   private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException {
185     return getErrorDetails(body, s -> {
186       TokenError gsonError = buildGson().fromJson(s, TokenError.class);
187       if (gsonError != null && gsonError.error != null) {
188         return gsonError.error;
189       }
190       return null;
191     });
192   }
193
194   private static ErrorDetails getErrorDetails(@Nullable ResponseBody body, UnaryOperator<String> parser) throws IOException {
195     if (body == null) {
196       return new ErrorDetails("", null);
197     }
198     String bodyStr = body.string();
199     if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
200       try {
201         return new ErrorDetails(bodyStr, parser.apply(bodyStr));
202       } catch (JsonParseException e) {
203         //ignore
204       }
205     }
206     return new ErrorDetails(bodyStr, null);
207   }
208
209   private static void handleError(Response response, boolean throwErrorDetails) throws IOException {
210     int responseCode = response.code();
211     ErrorDetails error = getError(response.body());
212     if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
213       String errorMsg = error.parsedErrorMsg != null ? error.parsedErrorMsg : "";
214       if (throwErrorDetails) {
215         throw new IllegalArgumentException(errorMsg);
216       } else {
217         throw new NotFoundException(errorMsg);
218       }
219     }
220     LOG.info(UNABLE_TO_CONTACT_BBC_SERVERS + ": {} {}", responseCode, error.parsedErrorMsg != null ? error.parsedErrorMsg : error.body);
221
222     if (throwErrorDetails && error.parsedErrorMsg != null) {
223       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + error.parsedErrorMsg);
224     } else {
225       throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS);
226     }
227   }
228
229   private static ErrorDetails getError(@Nullable ResponseBody body) throws IOException {
230     return getErrorDetails(body, s -> {
231       Error gsonError = buildGson().fromJson(s, Error.class);
232       if (gsonError != null && gsonError.errorMsg != null && gsonError.errorMsg.message != null) {
233         return gsonError.errorMsg.message;
234       }
235       return null;
236     });
237   }
238
239   private static class ErrorDetails {
240     private String body;
241     @Nullable
242     private String parsedErrorMsg;
243
244     public ErrorDetails(String body, @Nullable String parsedErrorMsg) {
245       this.body = body;
246       this.parsedErrorMsg = parsedErrorMsg;
247     }
248   }
249 }