Skip to content

Commit a5a8dcb

Browse files
[JENKINS-74970] Advise if Bitbucket Server rejects build status
If this plugin posts a build status to Bitbucket Server or Data Center, but Bitbucket responds with HTTP status 401 (Unauthorized) or 403 (Forbidden), then log extra information to help users grant Jenkins the required permission on the repository: * The permission that is needed: REPO_READ. * The Bitbucket user name. Currently, this comes from the "X-AUSERNAME" response header field. * The repository to which the request was sent. * The project or user that owns the repository. Because BitbucketServerAPIClient does not have direct access to a TaskLogger, it adds this information to the message of the BitbucketRequestException, and the caller then logs it from there. The behaviour on Bitbucket Cloud is unchanged.
1 parent e2da34a commit a5a8dcb

File tree

3 files changed

+89
-4
lines changed

3 files changed

+89
-4
lines changed

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.apache.commons.lang.StringUtils;
4848
import org.apache.http.Header;
4949
import org.apache.http.HttpHost;
50+
import org.apache.http.HttpResponse;
5051
import org.apache.http.HttpStatus;
5152
import org.apache.http.NameValuePair;
5253
import org.apache.http.auth.AuthScope;
@@ -97,9 +98,14 @@ protected String truncateMiddle(@CheckForNull String value, int maxLength) {
9798
}
9899
}
99100

100-
protected BitbucketRequestException buildResponseException(CloseableHttpResponse response, String errorMessage) {
101+
protected BitbucketRequestException buildResponseException(CloseableHttpResponse response,
102+
String errorMessage,
103+
@CheckForNull String advice) {
101104
String headers = StringUtils.join(response.getAllHeaders(), "\n");
102105
String message = String.format("HTTP request error.%nStatus: %s%nResponse: %s%n%s", response.getStatusLine(), errorMessage, headers);
106+
if (advice != null) {
107+
message = advice + System.lineSeparator() + message;
108+
}
103109
return new BitbucketRequestException(response.getStatusLine().getStatusCode(), message);
104110
}
105111

@@ -234,6 +240,12 @@ protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase htt
234240
}
235241

236242
protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException {
243+
return doRequest(request, requireAuthentication, HttpErrorAdvisor.NULL);
244+
}
245+
246+
protected String doRequest(HttpRequestBase request,
247+
boolean requireAuthentication,
248+
HttpErrorAdvisor advisor) throws IOException {
237249
try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) {
238250
int statusCode = response.getStatusLine().getStatusCode();
239251
if (statusCode == HttpStatus.SC_NOT_FOUND) {
@@ -247,7 +259,7 @@ protected String doRequest(HttpRequestBase request, boolean requireAuthenticatio
247259
String content = getResponseContent(response);
248260
EntityUtils.consume(response.getEntity());
249261
if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
250-
throw buildResponseException(response, content);
262+
throw buildResponseException(response, content, advisor.getAdvice(response));
251263
}
252264
return content;
253265
} catch (BitbucketRequestException e) {
@@ -296,7 +308,7 @@ protected InputStream getRequestAsInputStream(String path) throws IOException {
296308
}
297309
if (statusCode != HttpStatus.SC_OK) {
298310
String content = getResponseContent(response);
299-
throw buildResponseException(response, content);
311+
throw buildResponseException(response, content, null);
300312
}
301313
return new ClosingConnectionInputStream(response, httpget, getConnectionManager());
302314
}
@@ -351,4 +363,25 @@ public void close() throws Exception {
351363
protected BitbucketAuthenticator getAuthenticator() {
352364
return authenticator;
353365
}
366+
367+
/**
368+
* REST API operation methods in classes derived from {@link AbstractBitbucketApi}
369+
* can implement this interface to explain to users why
370+
* {@link #doRequest(HttpRequestBase, boolean, HttpErrorAdvisor)} failed.
371+
*/
372+
@FunctionalInterface
373+
protected interface HttpErrorAdvisor {
374+
/**
375+
* Gets user-readable advice on why Bitbucket returned an error HTTP status.
376+
*
377+
* @param response The HTTP response from Bitbucket.
378+
* @return Advice to the user on why the request failed, or {@code null}.
379+
*/
380+
@CheckForNull String getAdvice(HttpResponse response);
381+
382+
/**
383+
* A trivial {@link HttpErrorAdvisor} implementation that never has advice.
384+
*/
385+
public static final HttpErrorAdvisor NULL = response -> null;
386+
}
354387
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package com.cloudbees.jenkins.plugins.bitbucket.server.client;
2525

26+
import com.cloudbees.jenkins.plugins.bitbucket.Messages;
2627
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
2728
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
2829
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
@@ -87,10 +88,15 @@
8788
import jenkins.scm.api.SCMFile;
8889
import org.apache.commons.io.IOUtils;
8990
import org.apache.commons.lang.StringUtils;
91+
import org.apache.http.Header;
9092
import org.apache.http.HttpHost;
93+
import org.apache.http.HttpResponse;
9194
import org.apache.http.HttpStatus;
9295
import org.apache.http.client.methods.HttpGet;
96+
import org.apache.http.client.methods.HttpPost;
9397
import org.apache.http.conn.HttpClientConnectionManager;
98+
import org.apache.http.entity.ContentType;
99+
import org.apache.http.entity.StringEntity;
94100
import org.apache.http.impl.client.CloseableHttpClient;
95101
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
96102
import org.apache.http.message.BasicNameValuePair;
@@ -509,7 +515,11 @@ public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOExcep
509515
.set("repo", repositoryName)
510516
.set("hash", newStatus.getHash())
511517
.expand();
512-
postRequest(url, JsonParser.toJson(newStatus));
518+
519+
HttpPost request = new HttpPost(url);
520+
request.setEntity(new StringEntity(JsonParser.toJson(newStatus),
521+
ContentType.create("application/json", "UTF-8")));
522+
doRequest(request, true, this::adviceForBuildStatusError);
513523
}
514524

515525
/**
@@ -1051,4 +1061,45 @@ private Map<String,Object> collectLines(String response, final List<String> line
10511061
return content;
10521062
}
10531063

1064+
// Gets user-visible advice for an HTTP error response when
1065+
// Bitbucket Server rejects a build status.
1066+
// Implements AbstractBitbucketApi.HttpErrorAdvisor#getAdvice.
1067+
@CheckForNull
1068+
private String adviceForBuildStatusError(HttpResponse response) {
1069+
// If the HTTP request failed because of an authorization
1070+
// problem, then make the exception message also show the
1071+
// Bitbucket user name with which Jenkins authenticated,
1072+
// the project name, and the repository name.
1073+
//
1074+
// Such an authorization problem can occur especially in a
1075+
// pull request from a personal fork: if Jenkins has been
1076+
// granted REPO_READ access on the target repository of the PR
1077+
// but no access on the fork, then it can read the PR
1078+
// information from the target repository and check out the
1079+
// files, but cannot post a build status to the fork.
1080+
// Showing the name of the fork will help the user or
1081+
// administrator grant the required access.
1082+
//
1083+
// If the HTTP request already includes valid credentials,
1084+
// but the Bitbucket user has not been granted access on the
1085+
// repository, then Bitbucket Server responds with HTTP status
1086+
// 401 (Unauthorized) and a WWW-Authenticate header field that
1087+
// requests OAuth, even though RFC 7235 section 2.1 recommends
1088+
// 403 (Forbidden). Let's recognize both 401 and 403.
1089+
int httpStatus = response.getStatusLine().getStatusCode();
1090+
if (httpStatus == HttpStatus.SC_UNAUTHORIZED || httpStatus == HttpStatus.SC_FORBIDDEN) {
1091+
Header userNameHeader = response.getFirstHeader("X-AUSERNAME");
1092+
if (userNameHeader != null
1093+
&& !userNameHeader.getValue().equals("anonymous")) {
1094+
// Posting a build status requires REPO_READ access.
1095+
// https://docs.atlassian.com/bitbucket-server/rest/7.4.0/bitbucket-rest.html#idp219
1096+
return Messages.BitbucketServerAPIClient_adviceForBuildStatusError(
1097+
userNameHeader.getValue(),
1098+
getUserCentricOwner(),
1099+
getRepositoryName());
1100+
}
1101+
}
1102+
1103+
return null;
1104+
}
10541105
}

src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ BitbucketTagSCMHead.Pronoun=Tag
6565
TagDiscoveryTrait.authorityDisplayName=Trust origin tags
6666
BitbucketBuildStatusNotificationsTrait.displayName=Bitbucket build status notifications
6767
DiscardOldBranchTrait.displayName=Discard branch older than given days
68+
BitbucketServerAPIClient.adviceForBuildStatusError=Please verify that the Bitbucket user "{0}" is granted REPO_READ access on the repository "{1}/{2}".

0 commit comments

Comments
 (0)