Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-74970] Log user name and repository if Bitbucket Server forbids posting a build status #965

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
Expand Down Expand Up @@ -97,9 +98,14 @@
}
}

protected BitbucketRequestException buildResponseException(CloseableHttpResponse response, String errorMessage) {
protected BitbucketRequestException buildResponseException(CloseableHttpResponse response,
String errorMessage,
@CheckForNull String advice) {
String headers = StringUtils.join(response.getAllHeaders(), "\n");
String message = String.format("HTTP request error.%nStatus: %s%nResponse: %s%n%s", response.getStatusLine(), errorMessage, headers);
if (advice != null) {

Check warning on line 106 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 106 is only partially covered, one branch is missing
message = advice + System.lineSeparator() + message;

Check warning on line 107 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 107 is not covered by tests
}
return new BitbucketRequestException(response.getStatusLine().getStatusCode(), message);
}

Expand Down Expand Up @@ -234,6 +240,12 @@
}

protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException {
return doRequest(request, requireAuthentication, HttpErrorAdvisor.NULL);
}

protected String doRequest(HttpRequestBase request,
boolean requireAuthentication,
HttpErrorAdvisor advisor) throws IOException {
try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_NOT_FOUND) {
Expand All @@ -247,7 +259,7 @@
String content = getResponseContent(response);
EntityUtils.consume(response.getEntity());
if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
throw buildResponseException(response, content);
throw buildResponseException(response, content, advisor.getAdvice(response));
}
return content;
} catch (BitbucketRequestException e) {
Expand Down Expand Up @@ -296,7 +308,7 @@
}
if (statusCode != HttpStatus.SC_OK) {
String content = getResponseContent(response);
throw buildResponseException(response, content);
throw buildResponseException(response, content, null);

Check warning on line 311 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 311 is not covered by tests
}
return new ClosingConnectionInputStream(response, httpget, getConnectionManager());
}
Expand Down Expand Up @@ -351,4 +363,25 @@
protected BitbucketAuthenticator getAuthenticator() {
return authenticator;
}

/**
* REST API operation methods in classes derived from {@link AbstractBitbucketApi}
* can implement this interface to explain to users why
* {@link #doRequest(HttpRequestBase, boolean, HttpErrorAdvisor)} failed.
*/
@FunctionalInterface
protected interface HttpErrorAdvisor {
/**
* Gets user-readable advice on why Bitbucket returned an error HTTP status.
*
* @param response The HTTP response from Bitbucket.
* @return Advice to the user on why the request failed, or {@code null}.
*/
@CheckForNull String getAdvice(HttpResponse response);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is HttpResponse rather than CloseableHttpResponse because the method should not close the response.


/**
* A trivial {@link HttpErrorAdvisor} implementation that never has advice.
*/
public static final HttpErrorAdvisor NULL = response -> null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.cloudbees.jenkins.plugins.bitbucket.server.client;

import com.cloudbees.jenkins.plugins.bitbucket.Messages;
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
Expand Down Expand Up @@ -87,10 +88,15 @@
import jenkins.scm.api.SCMFile;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
Expand Down Expand Up @@ -509,7 +515,11 @@
.set("repo", repositoryName)
.set("hash", newStatus.getHash())
.expand();
postRequest(url, JsonParser.toJson(newStatus));

HttpPost request = new HttpPost(url);
request.setEntity(new StringEntity(JsonParser.toJson(newStatus),
ContentType.create("application/json", "UTF-8")));
doRequest(request, true, this::adviceForBuildStatusError);
}

/**
Expand Down Expand Up @@ -1051,4 +1061,45 @@
return content;
}

// Gets user-visible advice for an HTTP error response when
// Bitbucket Server rejects a build status.
// Implements AbstractBitbucketApi.HttpErrorAdvisor#getAdvice.
@CheckForNull
private String adviceForBuildStatusError(HttpResponse response) {
// If the HTTP request failed because of an authorization
// problem, then make the exception message also show the
// Bitbucket user name with which Jenkins authenticated,
// the project name, and the repository name.
//
// Such an authorization problem can occur especially in a
// pull request from a personal fork: if Jenkins has been
// granted REPO_READ access on the target repository of the PR
// but no access on the fork, then it can read the PR
// information from the target repository and check out the
// files, but cannot post a build status to the fork.
// Showing the name of the fork will help the user or
// administrator grant the required access.
//
// If the HTTP request already includes valid credentials,
// but the Bitbucket user has not been granted access on the
// repository, then Bitbucket Server responds with HTTP status
// 401 (Unauthorized) and a WWW-Authenticate header field that
// requests OAuth, even though RFC 7235 section 2.1 recommends
// 403 (Forbidden). Let's recognize both 401 and 403.
int httpStatus = response.getStatusLine().getStatusCode();
if (httpStatus == HttpStatus.SC_UNAUTHORIZED || httpStatus == HttpStatus.SC_FORBIDDEN) {
Header userNameHeader = response.getFirstHeader("X-AUSERNAME");
if (userNameHeader != null
&& !userNameHeader.getValue().equals("anonymous")) {
// Posting a build status requires REPO_READ access.
// https://docs.atlassian.com/bitbucket-server/rest/7.4.0/bitbucket-rest.html#idp219
return Messages.BitbucketServerAPIClient_adviceForBuildStatusError(
userNameHeader.getValue(),
getUserCentricOwner(),
getRepositoryName());
}
}

return null;

Check warning on line 1103 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 1089-1103 are not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ BitbucketTagSCMHead.Pronoun=Tag
TagDiscoveryTrait.authorityDisplayName=Trust origin tags
BitbucketBuildStatusNotificationsTrait.displayName=Bitbucket build status notifications
DiscardOldBranchTrait.displayName=Discard branch older than given days
BitbucketServerAPIClient.adviceForBuildStatusError=Please verify that the Bitbucket user "{0}" is granted REPO_READ access on the repository "{1}/{2}".
Loading