From bdfb81833a97da7014e13792f2d269c75a9d5041 Mon Sep 17 00:00:00 2001 From: Michal Duda Date: Tue, 2 Feb 2021 14:50:33 +0100 Subject: [PATCH] SONAR-14395 Validate permissions for BitBucket PR decoration settings --- .../BitbucketCloudRestClient.java | 166 ++++++++++-------- .../bitbucket/bitbucketcloud/Token.java | 2 +- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java index 1f3cc008f6a..9f04b336b9e 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java @@ -37,7 +37,6 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; -import org.sonar.api.internal.apachecommons.io.IOUtils; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; @@ -73,47 +72,58 @@ public class BitbucketCloudRestClient { * Validate parameters provided. */ public void validate(String clientId, String clientSecret, String workspace) { - String accessToken = validateAccessToken(clientId, clientSecret); - doGet(accessToken, buildUrl("/workspaces/" + workspace + "/permissions"), r -> null, true); - } + Token token = validateAccessToken(clientId, clientSecret); + + if (token.getScopes() == null || !token.getScopes().contains("pullrequest")) { + String msg = "The OAuth consumer in the Bitbucket workspace is not configured with the permission to read pull requests"; + LOG.info("Validation failed. {}}: {}", msg, token.getScopes()); + throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + msg); + } - private String validateAccessToken(String clientId, String clientSecret) { - Response response = null; try { - Request request = createAccessTokenRequest(clientId, clientSecret); - response = client.newCall(request).execute(); + doGet(token.getAccessToken(), buildUrl("/repositories/" + workspace), r -> null); + } catch (NotFoundException | IllegalStateException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private Token validateAccessToken(String clientId, String clientSecret) { + Request request = createAccessTokenRequest(clientId, clientSecret); + try (Response response = client.newCall(request).execute()) { if (response.isSuccessful()) { - return buildGson().fromJson(response.body().charStream(), Token.class).getAccessToken(); + return buildGson().fromJson(response.body().charStream(), Token.class); } ErrorDetails errorMsg = getTokenError(response.body()); - if (errorMsg.parsedErrorMsg != null) { - switch (errorMsg.parsedErrorMsg) { + if (errorMsg.body != null) { + switch (errorMsg.body) { case "invalid_grant": throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Configure the OAuth consumer in the Bitbucket workspace to be a private consumer"); case "unauthorized_client": throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Check your credentials"); default: - LOG.info("Validation failed: " + errorMsg.parsedErrorMsg); + if (errorMsg.parsedErrorMsg != null) { + LOG.info("Validation failed: " + errorMsg.parsedErrorMsg); + throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + errorMsg.parsedErrorMsg); + } else { + LOG.info("Validation failed: " + errorMsg.body); + throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS); + } } } else { - LOG.info("Validation failed: " + errorMsg.body); + LOG.info("Validation failed"); } throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS); } catch (IOException e) { throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e); - } finally { - if (response != null && response.body() != null) { - IOUtils.closeQuietly(response); - } } } public String createAccessToken(String clientId, String clientSecret) { Request request = createAccessTokenRequest(clientId, clientSecret); - return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class), false).getAccessToken(); + return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class)).getAccessToken(); } private Request createAccessTokenRequest(String clientId, String clientSecret) { @@ -133,35 +143,31 @@ public class BitbucketCloudRestClient { return HttpUrl.parse(removeEnd(bitbucketCloudEndpoint, "/") + "/" + VERSION + relativeUrl); } - protected G doGet(String accessToken, HttpUrl url, Function handler, boolean throwErrorDetails) { - Request request = prepareRequestWithAccessToken(accessToken, GET, url, null); - return doCall(request, handler, throwErrorDetails); - } - protected G doGet(String accessToken, HttpUrl url, Function handler) { - return doGet(accessToken, url, handler, false); + Request request = prepareRequestWithAccessToken(accessToken, GET, url, null); + return doCall(request, handler); } protected void doPost(String accessToken, HttpUrl url, RequestBody body) { Request request = prepareRequestWithAccessToken(accessToken, "POST", url, body); - doCall(request, r -> null, false); + doCall(request, r -> null); } protected void doPut(String accessToken, HttpUrl url, String json) { RequestBody body = RequestBody.create(json, JSON_MEDIA_TYPE); Request request = prepareRequestWithAccessToken(accessToken, "PUT", url, body); - doCall(request, r -> null, false); + doCall(request, r -> null); } protected void doDelete(String accessToken, HttpUrl url) { Request request = prepareRequestWithAccessToken(accessToken, "DELETE", url, null); - doCall(request, r -> null, false); + doCall(request, r -> null); } - private G doCall(Request request, Function handler, boolean throwErrorDetails) { + private G doCall(Request request, Function handler) { try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { - handleError(response, throwErrorDetails); + handleError(response); } return handler.apply(response); } catch (IOException e) { @@ -169,58 +175,17 @@ public class BitbucketCloudRestClient { } } - private static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) { - return new Request.Builder() - .method(method, body) - .url(url) - .header("Authorization", "Bearer " + accessToken) - .build(); - } - - public static Gson buildGson() { - return new GsonBuilder().create(); - } - - private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException { - return getErrorDetails(body, s -> { - TokenError gsonError = buildGson().fromJson(s, TokenError.class); - if (gsonError != null && gsonError.error != null) { - return gsonError.error; - } - return null; - }); - } - - private static ErrorDetails getErrorDetails(@Nullable ResponseBody body, UnaryOperator parser) throws IOException { - if (body == null) { - return new ErrorDetails("", null); - } - String bodyStr = body.string(); - if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) { - try { - return new ErrorDetails(bodyStr, parser.apply(bodyStr)); - } catch (JsonParseException e) { - //ignore - } - } - return new ErrorDetails(bodyStr, null); - } - - private static void handleError(Response response, boolean throwErrorDetails) throws IOException { + private static void handleError(Response response) throws IOException { int responseCode = response.code(); ErrorDetails error = getError(response.body()); if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { String errorMsg = error.parsedErrorMsg != null ? error.parsedErrorMsg : ""; - if (throwErrorDetails) { - throw new IllegalArgumentException(errorMsg); - } else { - throw new NotFoundException(errorMsg); - } + throw new NotFoundException(errorMsg); } LOG.info(UNABLE_TO_CONTACT_BBC_SERVERS + ": {} {}", responseCode, error.parsedErrorMsg != null ? error.parsedErrorMsg : error.body); - if (throwErrorDetails && error.parsedErrorMsg != null) { - throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + error.parsedErrorMsg); + if (error.parsedErrorMsg != null) { + throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + error.parsedErrorMsg); } else { throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS); } @@ -236,14 +201,61 @@ public class BitbucketCloudRestClient { }); } + private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException { + if (body == null) { + return new ErrorDetails(null, null); + } + String bodyStr = body.string(); + if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) { + try { + TokenError gsonError = buildGson().fromJson(bodyStr, TokenError.class); + if (gsonError != null && gsonError.error != null) { + return new ErrorDetails(gsonError.error, gsonError.errorDescription); + } + } catch (JsonParseException e) { + // ignore + } + } + + return new ErrorDetails(bodyStr, null); + } + private static class ErrorDetails { - private String body; @Nullable - private String parsedErrorMsg; + private final String body; + @Nullable + private final String parsedErrorMsg; - public ErrorDetails(String body, @Nullable String parsedErrorMsg) { + public ErrorDetails(@Nullable String body, @Nullable String parsedErrorMsg) { this.body = body; this.parsedErrorMsg = parsedErrorMsg; } } + + private static ErrorDetails getErrorDetails(@Nullable ResponseBody body, UnaryOperator parser) throws IOException { + if (body == null) { + return new ErrorDetails("", null); + } + String bodyStr = body.string(); + if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) { + try { + return new ErrorDetails(bodyStr, parser.apply(bodyStr)); + } catch (JsonParseException e) { + // ignore + } + } + return new ErrorDetails(bodyStr, null); + } + + private static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) { + return new Request.Builder() + .method(method, body) + .url(url) + .header("Authorization", "Bearer " + accessToken) + .build(); + } + + public static Gson buildGson() { + return new GsonBuilder().create(); + } } diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Token.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Token.java index 2933ecf9486..fc5fec3c499 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Token.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Token.java @@ -22,7 +22,7 @@ package org.sonar.alm.client.bitbucket.bitbucketcloud; import com.google.gson.annotations.SerializedName; public class Token { - @SerializedName("scope") + @SerializedName("scopes") private String scopes; @SerializedName("access_token") private String accessToken; -- 2.39.5