Skip to content

Commit 166de2d

Browse files
committed
[JENKINS-74983] Add support for authenticated Webhooks registered in Bitbucket
Verify in the webhooks processor when the signature is present is matches the configured Add configuration in the global settings to setup HMAC credentials. The secret is not customisable by single project for the following reasons: * events should contains a duplicate of the payload to be verified only in BitbucketSCMSource.retrieve method. * that means spend a lot of resources just to ignore the payload. Multiple fake requests, would overload Jenkins that have to process events to lookup the right project. * could not response to Bitbucket that the payload is invalid because events are managed async. * each event could serve multiple projects that potentially could be configured with a different secret.
1 parent a13b7b3 commit 166de2d

33 files changed

+823
-198
lines changed

docs/USER_GUIDE.adoc

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ Follow these steps to create a multi-branch project with Bitbucket as a source:
4545
. Create the multi-branch project. This step depends on which multi-branch plugin is installed.
4646
For example, "Multibranch Pipeline" should be available as a project type if Pipeline Multibranch plugin is installed.
4747
+
48-
image::images/screenshot-1.png[scaledwidth=90%]
48+
image::images/screenshot-1.png
4949

5050
. Select "Bitbucket" as _Branch Source_
5151
+
52-
image::images/screenshot-2.png[scaledwidth=90%]
52+
image::images/screenshot-2.png
5353

5454
. Set credentials to access Bitbucket API and checkout sources (see "Credentials configuration" section below).
5555
. Set the repository owner and name that will be monitored for branches and pull requests.
5656
+
57-
image::images/screenshot-4.png[scaledwidth=90%]
57+
image::images/screenshot-4.png
5858

5959
. Finally, save the project. The initial indexing process will run and create projects for branches and pull requests.
6060
+
61-
image::images/screenshot-5.png[scaledwidth=90%]
61+
image::images/screenshot-5.png
6262

6363
[id=bitbucket-scm-navigator]
6464
== Organization folders
@@ -76,20 +76,20 @@ image::images/screenshot-6.png[scaledwidth=70%]
7676
.. A Bitbucket Data Center Project ID: all repositories in the project are imported as Multibranch projects. *Note that the project ID needs to be used instead of the project name*.
7777
.. A regular username: all repositories which the username is owner of are imported.
7878
+
79-
image::images/screenshot-8.png[scaledwidth=90%]
79+
image::images/screenshot-8.png
8080

8181
. Save the configuration. The initial indexing process starts. Once it finishes, a Multibranch
8282
project is created for each repository.
8383
+
84-
image::images/screenshot-9.png[scaledwidth=90%]
84+
image::images/screenshot-9.png
8585

8686
[id=bitbucket-avatar]
8787
== Avatar
8888

8989
This plugin have customized icon designed for the "Organization Folder" image:/src/main/webapp/images/bitbucket-logo.svg[icon,20,20], for "Multibranch Pipeline", for Single Repository image:/src/main/webapp/images/bitbucket-repository-git.svg[icon,20,20] and Folder image:/src/main/webapp/images/bitbucket-scmnavigator.svg[icon,20,20] project type. This is the default behaviour of the plugin starting from version 935.
9090
It is possible associate the Bitbucket avatar to the project item selecting the "Show Bitbucket avatar images" behaviour in the project configuration.
9191

92-
image::images/screenshot-19.png[scaledwidth=90%]
92+
image::images/screenshot-19.png
9393

9494
The supported avatars are:
9595

@@ -121,26 +121,38 @@ For Bitbucket Data Center only it is possible chose which webhooks implementatio
121121

122122
- 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._
123123

124-
image::images/screenshot-14.png[scaledwidth=90%]
124+
image::images/screenshot-14.png
125125

126126
For both Bitbucket _Multibranch Pipelines_ and _Organization folders_ there is an "Override hook management" behavior
127127
to opt out or adjust system-wide settings.
128128

129-
image::images/screenshot-18.png[scaledwidth=90%]
129+
image::images/screenshot-18.png
130130

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
135+
136+
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.
137+
To ensure your self-hosted server only processes deliveries from Bitbucket, you need to:
138+
* Create a secret token for a webhook
139+
* Enable hooks signature verification for the chosen Bitbucket Endpoints
140+
* Select the secret token create at point 1, only _String credentials_ are taken into account.
141+
142+
Any incoming webhook payloads from that given endpoint will be validated against the configured token, to verify they are coming from the configured Bitbucket endpoint URL.
143+
144+
image::images/screenshot-20.png
145+
134146
[id=bitbucket-creds-config]
135147
== Credentials configuration
136148

137149
The plugin (for both _Bitbucket multibranch pipelines_ and _Bitbucket Workspace/Project organization folders_) requires a credential to be configured to scan branches. It will also be the default credential to use when checking out sources.
138150

139-
image::images/screenshot-3.png[scaledwidth=90%]
151+
image::images/screenshot-3.png
140152

141153
As the `Checkout Credential` configuration was removed in commit (link:https://github.com/jenkinsci/bitbucket-branch-source-plugin/commit/a4c6bf39b83168ff62fc622bd4084ef90cf810c0[a4c6bf3]), you can alternatively add a `Checkout over SSH` behavior in the configuration of Behaviours, so that to configure a seperate SSH credential for checking out sources.
142154

143-
image::images/screenshot-7.png[scaledwidth=90%]
155+
image::images/screenshot-7.png
144156

145157
=== Access Token
146158

@@ -154,13 +166,13 @@ First, create a new _access token_ in Bitbucket as instructed in one of the foll
154166

155167
At least allow _read_ access for repositories. If you want the plugin to install the webhooks, allow _Read and write_ access for Webhooks.
156168

157-
image::images/screenshot-16.png[scaledwidth=90%]
169+
image::images/screenshot-16.png
158170

159171
Then create a new _Secret text_ credential in Jenkins, enter the Bitbucket token generated in the previous steps in the _Secret_ field.
160172

161173
If you want be able to perform git push operation from CLI than you have to setup _write_ access for repositories. Than configure the _Custom user name/e-mail address_ trait with the Repository Access Token email generated when you created the Repository Access Token (for example, [email protected]).
162174

163-
image::images/screenshot-17.png[scaledwidth=90%]
175+
image::images/screenshot-17.png
164176

165177
=== Personal Access Token
166178

@@ -190,13 +202,13 @@ The plugin can make use of OAuth credentials (Bitbucket Cloud only) instead of t
190202
First create a new _OAuth consumer_ in Bitbucket as instructed in the https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html[Bitbucket OAuth Documentation].
191203
Don't forget to check _This is a private consumer_ and at least allow _read_ access for repositories and pull requests. If you want the plugin to install the webhooks, also allow _read_ and _write_ access for webhooks.
192204

193-
image::images/screenshot-10.png[scaledwidth=90%]
205+
image::images/screenshot-10.png
194206

195207
Then create new _Username with password credentials_ in Jenkins, enter the Bitbucket OAuth consumer key in the _Username_ field and the Bitbucket OAuth consumer secret in the _Password_ field.
196208

197-
image::images/screenshot-11.png[scaledwidth=90%]
209+
image::images/screenshot-11.png
198210

199-
image::images/screenshot-12.png[scaledwidth=90%]
211+
image::images/screenshot-12.png
200212

201213
[id=bitbucket-mirror-support]
202214
== Mirror support
@@ -213,14 +225,14 @@ Cloning from the mirror can only be used with native web-hooks since plugin web-
213225

214226
For branches and tags, the mirror sync event is used. Thus, at cloning time, the mirror is already synchronized. However, in the case of a pull request event, there is no such guarantee. The plugin optimistically assumes that the mirror is synced and the required commit hashes exist in the mirrored repository at cloning time. If the plugin can't find the required hashes, it falls back to the primary repository.
215227

216-
image::images/screenshot-13.png[scaledwidth=90%]
228+
image::images/screenshot-13.png
217229

218230
[id=bitbucket-build-status]
219231
== Bitbucket build status
220232

221233
When a new job build starts, the plugin send notifications to Bitbucket about the build status. An "In progress" notification is sent after complete the git checkout, another notification is sent at the end of the build, the sent value depends by the build result and the configuration given by the trait.
222234

223-
image::images/screenshot-15.png[scaledwidth=90%]
235+
image::images/screenshot-15.png
224236

225237
Follow a summary of all possible values:
226238

docs/images/screenshot-20.png

27.6 KB
Loading

pom.xml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
<!-- Jenkins.MANAGE is still in Beta -->
3636
<useBeta>true</useBeta>
3737
<tagNameFormat>@{project.version}</tagNameFormat>
38-
<!-- because eclipse generate a JUnit5 run configuration with placeholder not replaced in the jvm argument section! -->
39-
<surefire.forkNumber>1</surefire.forkNumber>
4038
</properties>
4139

4240
<developers>
@@ -259,4 +257,19 @@
259257
</plugins>
260258
</build>
261259

260+
<profiles>
261+
<profile>
262+
<id>eclipse</id>
263+
<activation>
264+
<property>
265+
<name>m2e.version</name>
266+
</property>
267+
</activation>
268+
<properties>
269+
<!-- because eclipse generate a JUnit5 run configuration with placeholder not replaced in the jvm argument section! -->
270+
<surefire.forkNumber>1</surefire.forkNumber>
271+
<jenkins.javaAgent />
272+
</properties>
273+
</profile>
274+
</profiles>
262275
</project>

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ public String getServerUrl() {
221221

222222
@DataBoundSetter
223223
public void setServerUrl(@CheckForNull String serverUrl) {
224-
serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl);
224+
serverUrl = BitbucketEndpointConfiguration.normalizeServerURL(serverUrl);
225225
if (serverUrl != null && !StringUtils.equals(this.serverUrl, serverUrl)) {
226226
this.serverUrl = serverUrl;
227227
resetId();

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public String getServerUrl() {
287287

288288
@DataBoundSetter
289289
public void setServerUrl(@CheckForNull String serverUrl) {
290-
String url = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl);
290+
String url = BitbucketEndpointConfiguration.normalizeServerURL(serverUrl);
291291
if (url == null) {
292292
url = BitbucketEndpointConfiguration.get().getDefaultEndpoint().getServerUrl();
293293
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,9 @@ boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path)
214214
*
215215
* @return the list of webhooks registered in the repository.
216216
* @throws IOException if there was a network communications error.
217-
* @throws InterruptedException if interrupted while waiting on remote communications.
218217
*/
219218
@NonNull
220-
List<? extends BitbucketWebHook> getWebHooks() throws IOException, InterruptedException;
219+
List<? extends BitbucketWebHook> getWebHooks() throws IOException;
221220

222221
/**
223222
* Returns the team of the current owner or {@code null} if the current owner is not a team.

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketWebHook.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package com.cloudbees.jenkins.plugins.bitbucket.api;
2525

26+
import edu.umd.cs.findbugs.annotations.Nullable;
2627
import java.util.List;
2728

2829
/**
@@ -55,4 +56,15 @@ public interface BitbucketWebHook {
5556
*/
5657
String getUuid();
5758

59+
/**
60+
* Returns the secret used as the key to generate a HMAC digest value sent
61+
* in the X-Hub-Signature header at delivery time.
62+
*
63+
* @return a secret
64+
*/
65+
@Nullable
66+
default String getSecret() {
67+
return null;
68+
}
69+
5870
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketRepositoryHook.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class BitbucketRepositoryHook implements BitbucketWebHook {
3434

3535
private String url;
3636

37+
private String secret;
38+
3739
private boolean active;
3840

3941
private List<String> events;
@@ -83,4 +85,12 @@ public void setUuid(String uuid) {
8385
this.uuid = uuid;
8486
}
8587

88+
public String getSecret() {
89+
return secret;
90+
}
91+
92+
public void setSecret(String secret) {
93+
this.secret = secret;
94+
}
95+
8696
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,22 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.endpoints;
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
27-
import com.cloudbees.plugins.credentials.CredentialsMatchers;
28-
import com.cloudbees.plugins.credentials.CredentialsProvider;
27+
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentials;
2928
import com.cloudbees.plugins.credentials.common.StandardCredentials;
3029
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
31-
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
3230
import edu.umd.cs.findbugs.annotations.CheckForNull;
3331
import edu.umd.cs.findbugs.annotations.NonNull;
3432
import hudson.Util;
3533
import hudson.model.AbstractDescribableImpl;
36-
import hudson.security.ACL;
3734
import jenkins.authentication.tokens.api.AuthenticationTokens;
3835
import jenkins.model.Jenkins;
3936
import org.apache.commons.lang3.StringUtils;
4037
import org.jenkinsci.plugins.displayurlapi.ClassicDisplayURLProvider;
38+
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
4139
import org.kohsuke.stapler.DataBoundSetter;
4240

41+
import static hudson.Util.fixEmptyAndTrim;
42+
4343
/**
4444
* Represents a {@link BitbucketCloudEndpoint} or a {@link BitbucketServerEndpoint}.
4545
*
@@ -58,6 +58,17 @@ public abstract class AbstractBitbucketEndpoint extends AbstractDescribableImpl<
5858
@CheckForNull
5959
private final String credentialsId;
6060

61+
/**
62+
* {@code true} if and only if Jenkins have to verify the signature of all incoming hooks.
63+
*/
64+
private boolean enableHookSignature;
65+
66+
/**
67+
* The {@link StringCredentials#getId()} of the credentials to use to verify the signature of hooks.
68+
*/
69+
@CheckForNull
70+
private String hookSignatureCredentialsId;
71+
6172
/**
6273
* Jenkins Server Root URL to be used by that Bitbucket endpoint.
6374
* The global setting from Jenkins.get().getRootUrl()
@@ -76,7 +87,7 @@ public abstract class AbstractBitbucketEndpoint extends AbstractDescribableImpl<
7687
*/
7788
AbstractBitbucketEndpoint(boolean manageHooks, @CheckForNull String credentialsId) {
7889
this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId);
79-
this.credentialsId = manageHooks ? credentialsId : null;
90+
this.credentialsId = manageHooks ? fixEmptyAndTrim(credentialsId) : null;
8091
}
8192

8293
/**
@@ -106,7 +117,7 @@ static String normalizeJenkinsRootUrl(String rootUrl) {
106117
// This routine is not really BitbucketEndpointConfiguration
107118
// specific, it just works on strings with some defaults:
108119
return Util.ensureEndsWith(
109-
BitbucketEndpointConfiguration.normalizeServerUrl(rootUrl),"/");
120+
BitbucketEndpointConfiguration.normalizeServerURL(rootUrl),"/");
110121
}
111122

112123
/**
@@ -124,7 +135,7 @@ public String getBitbucketJenkinsRootUrl() {
124135
@DataBoundSetter
125136
public void setBitbucketJenkinsRootUrl(String bitbucketJenkinsRootUrl) {
126137
if (manageHooks) {
127-
this.bitbucketJenkinsRootUrl = Util.fixEmptyAndTrim(bitbucketJenkinsRootUrl);
138+
this.bitbucketJenkinsRootUrl = fixEmptyAndTrim(bitbucketJenkinsRootUrl);
128139
if (this.bitbucketJenkinsRootUrl != null) {
129140
this.bitbucketJenkinsRootUrl = normalizeJenkinsRootUrl(this.bitbucketJenkinsRootUrl);
130141
}
@@ -133,6 +144,29 @@ public void setBitbucketJenkinsRootUrl(String bitbucketJenkinsRootUrl) {
133144
}
134145
}
135146

147+
@CheckForNull
148+
public String getHookSignatureCredentialsId() {
149+
return hookSignatureCredentialsId;
150+
}
151+
152+
@DataBoundSetter
153+
public void setHookSignatureCredentialsId(String hookSignatureCredentialsId) {
154+
if (enableHookSignature) {
155+
this.hookSignatureCredentialsId = fixEmptyAndTrim(hookSignatureCredentialsId);
156+
} else {
157+
this.hookSignatureCredentialsId = null;
158+
}
159+
}
160+
161+
public boolean isEnableHookSignature() {
162+
return enableHookSignature;
163+
}
164+
165+
@DataBoundSetter
166+
public void setEnableHookSignature(boolean enableHookSignature) {
167+
this.enableHookSignature = enableHookSignature;
168+
}
169+
136170
/**
137171
* Jenkins Server Root URL to be used by this Bitbucket endpoint.
138172
* The global setting from Jenkins.get().getRootUrl()
@@ -220,18 +254,17 @@ public final String getCredentialsId() {
220254
*/
221255
@CheckForNull
222256
public StandardCredentials credentials() {
223-
return StringUtils.isBlank(credentialsId) ? null : CredentialsMatchers.firstOrNull(
224-
CredentialsProvider.lookupCredentialsInItemGroup(
225-
StandardCredentials.class,
226-
Jenkins.get(),
227-
ACL.SYSTEM2 ,
228-
URIRequirementBuilder.fromUri(getServerUrl()).build()
229-
),
230-
CredentialsMatchers.allOf(
231-
CredentialsMatchers.withId(credentialsId),
232-
AuthenticationTokens.matcher(BitbucketAuthenticator.authenticationContext(getServerUrl()))
233-
)
234-
);
257+
return BitbucketCredentials.lookupCredentials(getServerUrl(), Jenkins.get(), credentialsId, StandardCredentials.class);
258+
}
259+
260+
/**
261+
* Looks up the {@link StringCredentials} to use to verify the signature of hooks.
262+
*
263+
* @return the credentials or {@code null}.
264+
*/
265+
@CheckForNull
266+
public StringCredentials hookSignatureCredentials() {
267+
return BitbucketCredentials.lookupCredentials(getServerUrl(), Jenkins.get(), hookSignatureCredentialsId, StringCredentials.class);
235268
}
236269

237270
/**

0 commit comments

Comments
 (0)