Skip to content

Commit 57f497f

Browse files
committed
[JENKINS-75735] Hook payload is discarded if not recognized and comes from Bitbucket Cloud and DataCenter instances
Change test cases to better reflect the header sent in the request of a webhook Add payload examples provided by Alexey P. from moveworkforward support team.
1 parent f4b8340 commit 57f497f

File tree

11 files changed

+652
-14
lines changed

11 files changed

+652
-14
lines changed

docs/USER_GUIDE.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ For Bitbucket Data Center only it is possible chose which webhooks implementatio
119119

120120
- Native implementation will configure the webhooks provided by default with the Server, so it will always be available.
121121

122-
- Plugin implementation relies on the configuration available via specific APIs provided by the https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?tab=overview&hosting=datacenter[Post Webhooks for Bitbucket] plugin itself. To get it worked plugin must be already pre-installed on the server instance. This provider allows custom settings managed by the _ignore committers_ trait. _Note: This specific implementation will be moved to an individual repository as soon as https://issues.jenkins.io/browse/JENKINS-74913[JENKINS-74913] is implemented._
122+
- Plugin implementation (*deprecated*) relies on the configuration available via specific APIs provided by the https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?tab=overview&hosting=datacenter[Post Webhooks for Bitbucket] plugin itself. To get it worked plugin must be already pre-installed on the server instance. This provider allows custom settings managed by the _ignore committers_ trait. _Note: This specific implementation will be moved to an individual repository as soon as https://issues.jenkins.io/browse/JENKINS-74913[JENKINS-74913] is implemented._
123123

124124
image::images/screenshot-14.png[]
125125

@@ -131,7 +131,7 @@ image::images/screenshot-18.png[]
131131
IMPORTANT: In order to have the auto-registering process working fine the Jenkins base URL must be
132132
properly configured in _Manage Jenkins_ » _System_
133133

134-
=== Webhooks signature
134+
=== Signature verification for incoming webhooks
135135

136136
Once Jenkins is configured to receive payloads, it will listen for any delivery that's sent to the endpoint you configured. For security reasons, you should only process deliveries from Bitbucket.
137137
To ensure your self-hosted server only processes deliveries from Bitbucket, you need to:

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,20 @@ public HttpResponse doNotify(StaplerRequest2 req) throws IOException {
107107
return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "X-Event-Key HTTP header invalid: " + eventKey);
108108
}
109109

110-
String bitbucketKey = req.getHeader("X-Bitbucket-Type");
110+
String bitbucketKey = req.getHeader("X-Bitbucket-Type"); // specific header from Plugin implementation
111111
String serverURL = req.getParameter("server_url");
112112

113113
BitbucketType instanceType = null;
114114
if (bitbucketKey != null) {
115115
instanceType = BitbucketType.fromString(bitbucketKey);
116116
}
117-
if (instanceType == null && serverURL != null) {
118-
LOGGER.log(Level.FINE, "server_url request parameter found. Bitbucket Native Server webhook incoming.");
119-
instanceType = BitbucketType.SERVER;
117+
if (serverURL != null) {
118+
if (instanceType == null) {
119+
LOGGER.log(Level.FINE, "server_url request parameter found. Bitbucket Native Server webhook incoming.");
120+
instanceType = BitbucketType.SERVER;
121+
} else {
122+
LOGGER.log(Level.FINE, "X-Bitbucket-Type header / server_url request parameter found. Bitbucket Plugin Server webhook incoming.");
123+
}
120124
} else {
121125
LOGGER.log(Level.FINE, "X-Bitbucket-Type header / server_url request parameter not found. Bitbucket Cloud webhook incoming.");
122126
instanceType = BitbucketType.CLOUD;
@@ -179,7 +183,7 @@ private HttpResponseException checkSignature(@NonNull StaplerRequest2 req, @NonN
179183
String requestId = ObjectUtils.firstNonNull(req.getHeader("X-Request-UUID"), req.getHeader("X-Request-Id"));
180184
String hookSignatureCredentialsId = endpoint.getHookSignatureCredentialsId();
181185
LOGGER.log(Level.WARNING, "No credentials {0} found to verify the signature of incoming webhook {1} request {2}", new Object[] { hookSignatureCredentialsId, hookId, requestId });
182-
return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "No credentials " + hookSignatureCredentialsId + " found to verify the signature");
186+
return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "No credentials " + hookSignatureCredentialsId + " found in Jenkins to verify the signature");
183187
}
184188
return null;
185189
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerWebhookImplementation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
/** The different webhook implementations available for Bitbucket Server. */
2929
public enum BitbucketServerWebhookImplementation implements ModelObject {
3030
/** Plugin-based webhooks. */
31-
PLUGIN("Plugin - Deprecated"),
31+
PLUGIN("Plugin"),
3232

3333
/** Native webhooks, available since Bitbucket Server 5.4. */
3434
NATIVE("Native");

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,21 @@ private void mockServerRequest(String serverURL) {
107107
when(req.getHeader("User-Agent")).thenReturn("Atlassian HttpClient 4.2.0 / Bitbucket-9.5.2 (9005002) / Default");
108108
}
109109

110+
private void mockPluginRequest(String serverURL) {
111+
when(req.getRemoteHost()).thenReturn("http://localhost:7990");
112+
when(req.getParameter("server_url")).thenReturn(serverURL);
113+
when(req.getRemoteAddr()).thenReturn("127.0.0.1");
114+
when(req.getScheme()).thenReturn("https");
115+
when(req.getServerName()).thenReturn("jenkins.example.com");
116+
when(req.getLocalPort()).thenReturn(80);
117+
when(req.getRequestURI()).thenReturn("/bitbucket-scmsource-hook/notify");
118+
when(req.getHeader("Content-Type")).thenReturn("application/json; charset=utf-8");
119+
when(req.getHeader("X-Bitbucket-Type")).thenReturn("server");
120+
when(req.getHeader("User-Agent")).thenReturn("Bitbucket version: 8.18.0, Post webhook plugin version: 7.13.41-SNAPSHOT");
121+
}
122+
110123
@Test
111-
void test_signature_is_missing_from_cloud_payload() throws Exception {
124+
void test_cloud_signature_is_missing() throws Exception {
112125
BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId);
113126
endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins");
114127
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
@@ -126,7 +139,7 @@ void test_signature_is_missing_from_cloud_payload() throws Exception {
126139
}
127140

128141
@Test
129-
void test_signature_from_cloud() throws Exception {
142+
void test_cloud_signature() throws Exception {
130143
mockCloudRequest();
131144
BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId);
132145
endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins");
@@ -150,7 +163,7 @@ void test_signature_from_cloud() throws Exception {
150163
}
151164

152165
@Test
153-
void test_bad_signature_from_cloud() throws Exception {
166+
void test_cloud_bad_signature() throws Exception {
154167
BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId);
155168
endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins");
156169
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
@@ -169,7 +182,7 @@ void test_bad_signature_from_cloud() throws Exception {
169182
}
170183

171184
@Test
172-
void test_signature_from_native_server() throws Exception {
185+
void test_native_signature() throws Exception {
173186
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, true, credentialsId);
174187
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
175188
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
@@ -202,7 +215,7 @@ void test_signature_from_native_server() throws Exception {
202215
}
203216

204217
@Test
205-
void test_ping_from_native_server() throws Exception {
218+
void test_native_ping() throws Exception {
206219
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null);
207220
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
208221
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
@@ -225,7 +238,102 @@ void test_ping_from_native_server() throws Exception {
225238
}
226239

227240
@Test
228-
void test_bad_signature_from_native_server() throws Exception {
241+
void test_plugin_pullrequest_created() throws Exception {
242+
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null);
243+
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
244+
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
245+
246+
try {
247+
mockPluginRequest(endpoint.getServerUrl());
248+
when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:created");
249+
when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_created.json"));
250+
251+
sut.doNotify(req);
252+
verify(hookProcessor).process(
253+
eq(HookEventType.PULL_REQUEST_CREATED),
254+
anyString(),
255+
eq(BitbucketType.SERVER),
256+
eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"),
257+
eq(endpoint.getServerUrl()));
258+
} finally {
259+
BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl());
260+
}
261+
}
262+
263+
@Test
264+
void test_plugin_pullrequest_updated() throws Exception {
265+
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null);
266+
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
267+
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
268+
269+
try {
270+
mockPluginRequest(endpoint.getServerUrl());
271+
when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:updated");
272+
when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_updated.json"));
273+
274+
sut.doNotify(req);
275+
verify(hookProcessor).process(
276+
eq(HookEventType.PULL_REQUEST_UPDATED),
277+
anyString(),
278+
eq(BitbucketType.SERVER),
279+
eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"),
280+
eq(endpoint.getServerUrl()));
281+
} finally {
282+
BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl());
283+
}
284+
}
285+
286+
@Test
287+
void test_plugin_pullrequest_merged() throws Exception {
288+
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null);
289+
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
290+
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
291+
292+
try {
293+
mockPluginRequest(endpoint.getServerUrl());
294+
when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:fulfilled");
295+
when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_merged.json"));
296+
297+
sut.doNotify(req);
298+
verify(hookProcessor).process(
299+
eq(HookEventType.PULL_REQUEST_MERGED),
300+
anyString(),
301+
eq(BitbucketType.SERVER),
302+
eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"),
303+
eq(endpoint.getServerUrl()));
304+
} finally {
305+
BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl());
306+
}
307+
}
308+
309+
@Test
310+
void test_plugin_create_branch() throws Exception {
311+
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null);
312+
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
313+
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
314+
315+
try {
316+
mockPluginRequest(endpoint.getServerUrl());
317+
when(req.getHeader("X-Event-Key")).thenReturn("repo:push");
318+
when(req.getInputStream()).thenReturn(loadResource("plugin/branch_created.json"));
319+
// when(req.getInputStream()).thenReturn(loadResource("plugin/branch_deleted.json"));
320+
// when(req.getInputStream()).thenReturn(loadResource("plugin/commit_update.json"));
321+
// when(req.getInputStream()).thenReturn(loadResource("plugin/commit_update2.json"));
322+
323+
sut.doNotify(req);
324+
verify(hookProcessor).process(
325+
eq(HookEventType.PUSH),
326+
anyString(),
327+
eq(BitbucketType.SERVER),
328+
eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"),
329+
eq(endpoint.getServerUrl()));
330+
} finally {
331+
BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl());
332+
}
333+
}
334+
335+
@Test
336+
void test_native_bad_signature() throws Exception {
229337
BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, true, credentialsId);
230338
endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com");
231339
BitbucketEndpointConfiguration.get().updateEndpoint(endpoint);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"actor": {
3+
"username": "admin",
4+
"displayName": "Administrator",
5+
"emailAddress": "admin@example.com"
6+
},
7+
"repository": {
8+
"scmId": "git",
9+
"project": {
10+
"key": "PROJECT_1",
11+
"name": "Project 1"
12+
},
13+
"slug": "rep_1",
14+
"links": {
15+
"self": [
16+
{
17+
"href": "http://localhost:7990/bitbucket/projects/PROJECT_1/repos/rep_1/browse"
18+
}
19+
]
20+
},
21+
"public": false,
22+
"ownerName": "PROJECT_1",
23+
"fullName": "PROJECT_1/rep_1",
24+
"owner": {
25+
"username": "PROJECT_1",
26+
"displayName": "PROJECT_1",
27+
"emailAddress": null
28+
}
29+
},
30+
"push": {
31+
"changes": [
32+
{
33+
"created": true,
34+
"closed": false,
35+
"new": {
36+
"type": "branch",
37+
"name": "test-webhook",
38+
"target": {
39+
"type": "commit",
40+
"hash": "417b2f673581ee6000e260a5fa65e62b56c7a3cd",
41+
"commitMessage": "Test webhooks"
42+
}
43+
},
44+
"old": null
45+
}
46+
]
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"actor": {
3+
"username": "admin",
4+
"displayName": "Administrator",
5+
"emailAddress": "admin@example.com"
6+
},
7+
"repository": {
8+
"scmId": "git",
9+
"project": {
10+
"key": "PROJECT_1",
11+
"name": "Project 1"
12+
},
13+
"slug": "rep_1",
14+
"links": {
15+
"self": [
16+
{
17+
"href": "http://localhost:7990/bitbucket/projects/PROJECT_1/repos/rep_1/browse"
18+
}
19+
]
20+
},
21+
"public": false,
22+
"ownerName": "PROJECT_1",
23+
"fullName": "PROJECT_1/rep_1",
24+
"owner": {
25+
"username": "PROJECT_1",
26+
"displayName": "PROJECT_1",
27+
"emailAddress": null
28+
}
29+
},
30+
"push": {
31+
"changes": [
32+
{
33+
"created": false,
34+
"closed": true,
35+
"new": null,
36+
"old": {
37+
"type": "branch",
38+
"name": "test-webhook",
39+
"target": {
40+
"type": "commit",
41+
"hash": "c0158b3e6c8cecf3bddc39d20957a98660cd23fd",
42+
"commitMessage": "Test webhook 2"
43+
}
44+
}
45+
}
46+
]
47+
}
48+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"actor": {
3+
"username": "admin",
4+
"displayName": "Administrator",
5+
"emailAddress": "admin@example.com"
6+
},
7+
"repository": {
8+
"scmId": "git",
9+
"project": {
10+
"key": "PROJECT_1",
11+
"name": "Project 1"
12+
},
13+
"slug": "rep_1",
14+
"links": {
15+
"self": [
16+
{
17+
"href": "http://localhost:7990/bitbucket/projects/PROJECT_1/repos/rep_1/browse"
18+
}
19+
]
20+
},
21+
"public": false,
22+
"ownerName": "PROJECT_1",
23+
"fullName": "PROJECT_1/rep_1",
24+
"owner": {
25+
"username": "PROJECT_1",
26+
"displayName": "PROJECT_1",
27+
"emailAddress": null
28+
}
29+
},
30+
"push": {
31+
"changes": [
32+
{
33+
"created": false,
34+
"closed": false,
35+
"new": {
36+
"type": "branch",
37+
"name": "test-webhook",
38+
"target": {
39+
"type": "commit",
40+
"hash": "c0158b3e6c8cecf3bddc39d20957a98660cd23fd",
41+
"commitMessage": "Test webhook 2"
42+
}
43+
},
44+
"old": {
45+
"type": "branch",
46+
"name": "test-webhook",
47+
"target": {
48+
"type": "commit",
49+
"hash": "417b2f673581ee6000e260a5fa65e62b56c7a3cd",
50+
"commitMessage": "Test webhooks"
51+
}
52+
}
53+
}
54+
]
55+
}
56+
}

0 commit comments

Comments
 (0)