Skip to content

Commit dcd189d

Browse files
authored
[JENKINS-75089] Getting clone URL using mirror returns HTTP 401 (#958)
Fix the method to retrieve the list of mirror URLs without using authentication. A JWT token is already provided in the request URL as documented at https://developer.atlassian.com/server/bitbucket/rest/v803/api-group-mirroring/#api-mirroring-latest-repos-repoid-mirrors-get
1 parent d3c8e0d commit dcd189d

File tree

5 files changed

+87
-15
lines changed

5 files changed

+87
-15
lines changed

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,21 @@ private void setClientProxyParams(String host, HttpClientBuilder builder) {
220220
@NonNull
221221
protected abstract CloseableHttpClient getClient();
222222

223-
/* for test purpose */
224-
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
225-
if (authenticator != null) {
223+
protected CloseableHttpResponse executeMethod(HttpHost host,
224+
HttpRequestBase httpMethod,
225+
boolean requireAuthentication) throws IOException {
226+
if (requireAuthentication && authenticator != null) {
226227
authenticator.configureRequest(httpMethod);
227228
}
228229
return getClient().execute(host, httpMethod, context);
229230
}
230231

231-
protected String doRequest(HttpRequestBase request) throws IOException {
232-
try (CloseableHttpResponse response = executeMethod(getHost(), request)) {
232+
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
233+
return executeMethod(host, httpMethod, true);
234+
}
235+
236+
protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException {
237+
try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) {
233238
int statusCode = response.getStatusLine().getStatusCode();
234239
if (statusCode == HttpStatus.SC_NOT_FOUND) {
235240
throw new FileNotFoundException("URL: " + request.getURI());
@@ -254,6 +259,10 @@ protected String doRequest(HttpRequestBase request) throws IOException {
254259
}
255260
}
256261

262+
protected String doRequest(HttpRequestBase request) throws IOException {
263+
return doRequest(request, true);
264+
}
265+
257266
private void release(HttpRequestBase method) {
258267
method.releaseConnection();
259268
HttpClientConnectionManager connectionManager = getConnectionManager();

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import org.apache.commons.lang.StringUtils;
9090
import org.apache.http.HttpHost;
9191
import org.apache.http.HttpStatus;
92+
import org.apache.http.client.methods.HttpGet;
9293
import org.apache.http.conn.HttpClientConnectionManager;
9394
import org.apache.http.impl.client.CloseableHttpClient;
9495
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@@ -464,7 +465,8 @@ public List<BitbucketMirroredRepositoryDescriptor> getMirrors(@NonNull Long repo
464465
*/
465466
@NonNull
466467
public BitbucketMirroredRepository getMirroredRepository(@NonNull String url) throws IOException, InterruptedException {
467-
String response = getRequest(url);
468+
HttpGet request = new HttpGet(url);
469+
String response = doRequest(request, false);
468470
try {
469471
return JsonParser.toJava(response, BitbucketMirroredRepository.class);
470472
} catch (IOException e) {

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ private BitbucketServerIntegrationClient(String payloadRootPath, String baseURL,
104104
}
105105

106106
@Override
107-
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
107+
protected CloseableHttpResponse executeMethod(HttpHost host,
108+
HttpRequestBase httpMethod,
109+
boolean requireAuthentication) throws IOException {
108110
String path = httpMethod.getURI().toString();
109111
audit.request(httpMethod);
110112

@@ -117,6 +119,11 @@ protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase htt
117119
return loadResponseFromResources(getClass(), path, payloadPath);
118120
}
119121

122+
@Override
123+
public BitbucketAuthenticator getAuthenticator() {
124+
return super.getAuthenticator();
125+
}
126+
120127
@Override
121128
public IRequestAudit getAudit() {
122129
return audit;
@@ -131,7 +138,7 @@ private static class BitbucketClouldIntegrationClient extends BitbucketCloudApiC
131138
private final IRequestAudit audit;
132139

133140
private BitbucketClouldIntegrationClient(String payloadRootPath, String owner, String repositoryName) {
134-
super(false, 0, 0, owner, null, repositoryName, (BitbucketAuthenticator) null);
141+
super(false, 0, 0, owner, null, repositoryName, mock(BitbucketAuthenticator.class));
135142

136143
if (payloadRootPath == null) {
137144
this.payloadRootPath = PAYLOAD_RESOURCE_ROOTPATH;
@@ -148,7 +155,9 @@ private BitbucketClouldIntegrationClient(String payloadRootPath, String owner, S
148155
}
149156

150157
@Override
151-
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
158+
protected CloseableHttpResponse executeMethod(HttpHost host,
159+
HttpRequestBase httpMethod,
160+
boolean requireAuthentication) throws IOException {
152161
String path = httpMethod.getURI().toString();
153162
audit.request(httpMethod);
154163

@@ -158,6 +167,11 @@ protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase htt
158167
return loadResponseFromResources(getClass(), path, payloadPath);
159168
}
160169

170+
@Override
171+
public BitbucketAuthenticator getAuthenticator() {
172+
return super.getAuthenticator();
173+
}
174+
161175
@Override
162176
public IRequestAudit getAudit() {
163177
return audit;

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package com.cloudbees.jenkins.plugins.bitbucket.server.client;
22

33
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
4+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
45
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
56
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status;
67
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository;
78
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory;
9+
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.BitbucketServerIntegrationClient;
810
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.IRequestAudit;
911
import io.jenkins.cli.shaded.org.apache.commons.lang.RandomStringUtils;
1012
import java.io.InputStream;
1113
import java.nio.charset.StandardCharsets;
1214
import java.util.List;
1315
import org.apache.commons.io.IOUtils;
16+
import org.apache.http.HttpRequest;
1417
import org.apache.http.client.methods.HttpHead;
1518
import org.apache.http.client.methods.HttpPost;
1619
import org.apache.http.client.methods.HttpRequestBase;
1720
import org.apache.http.impl.client.HttpClientBuilder;
21+
import org.junit.jupiter.api.BeforeAll;
1822
import org.junit.jupiter.api.Test;
1923
import org.jvnet.hudson.test.JenkinsRule;
2024
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
@@ -27,16 +31,25 @@
2731
import static org.hamcrest.Matchers.hasItem;
2832
import static org.hamcrest.Matchers.is;
2933
import static org.hamcrest.Matchers.not;
34+
import static org.mockito.ArgumentMatchers.any;
3035
import static org.mockito.Mockito.RETURNS_SELF;
3136
import static org.mockito.Mockito.mock;
3237
import static org.mockito.Mockito.mockStatic;
38+
import static org.mockito.Mockito.never;
3339
import static org.mockito.Mockito.verify;
3440

3541
@WithJenkins
3642
class BitbucketServerAPIClientTest {
3743

44+
private static JenkinsRule j;
45+
46+
@BeforeAll
47+
static void init(JenkinsRule rule) {
48+
j = rule;
49+
}
50+
3851
@Test
39-
void verify_status_notitication_name_max_length(JenkinsRule j) throws Exception {
52+
void verify_status_notitication_name_max_length() throws Exception {
4053
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.org");
4154
BitbucketBuildStatus status = new BitbucketBuildStatus();
4255
status.setName(RandomStringUtils.randomAlphanumeric(300));
@@ -63,8 +76,13 @@ private HttpRequestBase extractRequest(BitbucketApi client) {
6376
return captor.getValue();
6477
}
6578

79+
private BitbucketAuthenticator extractAuthenticator(BitbucketApi client) {
80+
assertThat(client).isInstanceOf(BitbucketServerIntegrationClient.class);
81+
return ((BitbucketServerIntegrationClient) client).getAuthenticator();
82+
}
83+
6684
@Test
67-
void verify_checkPathExists_given_a_path(JenkinsRule j) throws Exception {
85+
void verify_checkPathExists_given_a_path() throws Exception {
6886
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.org");
6987
assertThat(client.checkPathExists("feature/pipeline", "folder/Jenkinsfile")).isTrue();
7088

@@ -78,7 +96,7 @@ void verify_checkPathExists_given_a_path(JenkinsRule j) throws Exception {
7896
}
7997

8098
@Test
81-
void verify_checkPathExists_given_file(JenkinsRule j) throws Exception {
99+
void verify_checkPathExists_given_file() throws Exception {
82100
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.org");
83101
assertThat(client.checkPathExists("feature/pipeline", "Jenkinsfile")).isTrue();
84102

@@ -89,7 +107,7 @@ void verify_checkPathExists_given_file(JenkinsRule j) throws Exception {
89107
}
90108

91109
@Test
92-
void filterArchivedRepositories(JenkinsRule j) throws Exception {
110+
void filterArchivedRepositories() throws Exception {
93111
BitbucketApi client = BitbucketIntegrationClientFactory.getClient("localhost", "foo", "test-repos");
94112
List<? extends BitbucketRepository> repos = client.getRepositories();
95113
List<String> names = repos.stream().map(BitbucketRepository::getRepositoryName).toList();
@@ -98,20 +116,30 @@ void filterArchivedRepositories(JenkinsRule j) throws Exception {
98116
}
99117

100118
@Test
101-
void sortRepositoriesByName(JenkinsRule j) throws Exception {
119+
void sortRepositoriesByName() throws Exception {
102120
BitbucketApi client = BitbucketIntegrationClientFactory.getClient("localhost", "amuniz", "test-repos");
103121
List<? extends BitbucketRepository> repos = client.getRepositories();
104122
List<String> names = repos.stream().map(BitbucketRepository::getRepositoryName).toList();
105123
assertThat(names, is(List.of("another-repo", "dogs-repo", "test-repos")));
106124
}
107125

108126
@Test
109-
void disableCookieManager(JenkinsRule j) throws Exception {
127+
void disableCookieManager() throws Exception {
110128
try (MockedStatic<HttpClientBuilder> staticHttpClientBuilder = mockStatic(HttpClientBuilder.class)) {
111129
HttpClientBuilder httpClientBuilder = mock(HttpClientBuilder.class, RETURNS_SELF);
112130
staticHttpClientBuilder.when(HttpClientBuilder::create).thenReturn(httpClientBuilder);
113131
BitbucketIntegrationClientFactory.getClient("localhost", "amuniz", "test-repos");
114132
verify(httpClientBuilder).disableCookieManagement();
115133
}
116134
}
135+
136+
@Test
137+
void verify_mirroredRepository_does_not_authenticate_request() throws Exception {
138+
BitbucketServerAPIClient client = (BitbucketServerAPIClient) BitbucketIntegrationClientFactory.getClient("localhost", "amuniz", "test-repos");
139+
140+
BitbucketAuthenticator authenticator = extractAuthenticator(client);
141+
String url = "https://localhost/rest/mirroring/latest/upstreamServers/1/repos/1?jwt=TOKEN";
142+
client.getMirroredRepository(url);
143+
verify(authenticator, never()).configureRequest(any(HttpRequest.class));
144+
}
117145
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"pushUrls": [
3+
{
4+
"name": "http",
5+
"href": "https://bitbucket.example.com/scm/awesomeproject/awesomerepo.git"
6+
}
7+
],
8+
"cloneUrls": [
9+
{
10+
"name": "http",
11+
"href": "https://bitbucket.example.com/scm/awesomeproject/awesomerepo.git"
12+
}
13+
],
14+
"mirrorName": "Saigon Mirror",
15+
"lastUpdated": "<string>",
16+
"available": true,
17+
"status": "NOT_MIRRORED",
18+
"repositoryId": "1"
19+
}

0 commit comments

Comments
 (0)