Skip to content

Commit b9560d6

Browse files
authored
Migrate Webhook secret to String Credential and fix issue with persistent for JCasC (jenkinsci#267)
* Webhook secret is deleted by JCasC on every restart jenkinsci#162 Use Jenkins credentials for webhook secret. * Fix SpotBugs issues * Add migration methods and rename variable * Add readResolve and migrateWebhookSecretCredentials methods to be able to migrate old secret tokens to Jenkins credentials. * Rename secretTokenCredentialsId to webhookSecretCredentialsId * Fix checkstyle error * Rename help file * Fix indentation
1 parent 62ab6fc commit b9560d6

File tree

4 files changed

+148
-18
lines changed

4 files changed

+148
-18
lines changed

src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer.java

Lines changed: 143 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.cloudbees.plugins.credentials.CredentialsMatcher;
44
import com.cloudbees.plugins.credentials.CredentialsMatchers;
55
import com.cloudbees.plugins.credentials.CredentialsProvider;
6+
import com.cloudbees.plugins.credentials.CredentialsScope;
7+
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
68
import com.cloudbees.plugins.credentials.common.StandardCredentials;
79
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
810
import edu.umd.cs.findbugs.annotations.CheckForNull;
@@ -11,6 +13,8 @@
1113
import hudson.Util;
1214
import hudson.model.AbstractDescribableImpl;
1315
import hudson.model.Descriptor;
16+
import hudson.model.Item;
17+
import hudson.model.ItemGroup;
1418
import hudson.security.ACL;
1519
import hudson.security.AccessControlled;
1620
import hudson.util.FormValidation;
@@ -20,6 +24,8 @@
2024
import java.net.MalformedURLException;
2125
import java.net.URL;
2226
import java.security.SecureRandom;
27+
import java.util.Collections;
28+
import java.util.List;
2329
import java.util.logging.Level;
2430
import java.util.logging.Logger;
2531
import jenkins.model.Jenkins;
@@ -29,6 +35,8 @@
2935
import org.gitlab4j.api.GitLabApi;
3036
import org.gitlab4j.api.GitLabApiException;
3137
import org.gitlab4j.api.models.User;
38+
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
39+
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
3240
import org.kohsuke.accmod.Restricted;
3341
import org.kohsuke.accmod.restrictions.DoNotUse;
3442
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -126,8 +134,22 @@ public class GitLabServer extends AbstractDescribableImpl<GitLabServer> {
126134

127135
/**
128136
* The secret token used while setting up hook url in the GitLab server
137+
* @Deprecated Use webhookSecretCredentialsId instead
129138
*/
130-
private Secret secretToken;
139+
private transient Secret secretToken;
140+
141+
/**
142+
* The credentials id of the webhook secret token used while setting up hook url in the GitLab server
143+
*/
144+
@NonNull
145+
private String webhookSecretCredentialsId;
146+
147+
148+
/**
149+
* The credentials matcher for StringCredentials
150+
*/
151+
public static final CredentialsMatcher WEBHOOK_SECRET_CREDENTIALS_MATCHER = CredentialsMatchers
152+
.instanceOf(StringCredentials.class);
131153

132154
/**
133155
* {@code true} if and only if Jenkins should trigger a build immediately on a
@@ -159,6 +181,7 @@ public GitLabServer(@NonNull String serverUrl, @NonNull String name,
159181
? getRandomName()
160182
: StringUtils.trim(name);
161183
this.credentialsId = credentialsId;
184+
this.webhookSecretCredentialsId = "";
162185
}
163186

164187
/**
@@ -287,16 +310,53 @@ public String getHooksRootUrl() {
287310
return Util.ensureEndsWith(Util.fixEmptyAndTrim(hooksRootUrl), "/");
288311
}
289312

290-
@DataBoundSetter
313+
@DataBoundSetter @Deprecated
291314
public void setSecretToken(Secret token) {
292315
this.secretToken = token;
293316
}
294317

295-
// TODO: Use some UI element to trigger (what is the best way?)
296-
private void generateSecretToken() {
297-
byte[] random = new byte[16]; // 16x8=128bit worth of randomness, since we use md5 digest as the API token
298-
RANDOM.nextBytes(random);
299-
this.secretToken = Secret.decrypt(Util.toHexString(random));
318+
@DataBoundSetter
319+
public void setWebhookSecretCredentialsId(String token) {
320+
this.webhookSecretCredentialsId = token;
321+
}
322+
323+
public String getWebhookSecretCredentialsId() {
324+
return webhookSecretCredentialsId;
325+
}
326+
327+
/**
328+
* Looks up for StringCredentials
329+
*
330+
* @return {@link StringCredentials}
331+
*/
332+
public StringCredentials getWebhookSecretCredentials(AccessControlled context) {
333+
Jenkins jenkins = Jenkins.get();
334+
if (context == null) {
335+
jenkins.checkPermission(CredentialsProvider.USE_OWN);
336+
return StringUtils.isBlank(webhookSecretCredentialsId) ? null : CredentialsMatchers.firstOrNull( lookupCredentials(
337+
StringCredentials.class,
338+
jenkins,
339+
ACL.SYSTEM,
340+
fromUri(defaultIfBlank(serverUrl, GITLAB_SERVER_URL)).build()
341+
), withId(webhookSecretCredentialsId));
342+
} else {
343+
context.checkPermission(CredentialsProvider.USE_OWN);
344+
if (context instanceof ItemGroup) {
345+
return StringUtils.isBlank(webhookSecretCredentialsId) ? null : CredentialsMatchers.firstOrNull( lookupCredentials(
346+
StringCredentials.class,
347+
(ItemGroup) context,
348+
ACL.SYSTEM,
349+
fromUri(defaultIfBlank(serverUrl, GITLAB_SERVER_URL)).build()
350+
), withId(webhookSecretCredentialsId));
351+
} else {
352+
return StringUtils.isBlank(webhookSecretCredentialsId) ? null : CredentialsMatchers.firstOrNull( lookupCredentials(
353+
StringCredentials.class,
354+
(Item) context,
355+
ACL.SYSTEM,
356+
fromUri(defaultIfBlank(serverUrl, GITLAB_SERVER_URL)).build()
357+
), withId(webhookSecretCredentialsId));
358+
}
359+
}
300360
}
301361

302362
/**
@@ -307,15 +367,63 @@ public DescriptorImpl getDescriptor() {
307367
return (DescriptorImpl) super.getDescriptor();
308368
}
309369

370+
@Deprecated
310371
public Secret getSecretToken() {
311372
return secretToken;
312373
}
313374

375+
private StringCredentials getWebhookSecretCredentials(String webhookSecretCredentialsId) {
376+
Jenkins jenkins = Jenkins.get();
377+
jenkins.checkPermission(Jenkins.ADMINISTER);
378+
return StringUtils.isBlank(webhookSecretCredentialsId) ? null : CredentialsMatchers.firstOrNull(
379+
lookupCredentials(StringCredentials.class, jenkins),
380+
withId(webhookSecretCredentialsId)
381+
);
382+
}
383+
314384
public String getSecretTokenAsPlainText() {
315-
if (this.secretToken == null) {
385+
StringCredentials credentials = getWebhookSecretCredentials(webhookSecretCredentialsId);
386+
String secretToken = "";
387+
if (credentials != null) {
388+
secretToken = credentials.getSecret().getPlainText();
389+
} else {
316390
return null;
317391
}
318-
return this.secretToken.getPlainText();
392+
return secretToken;
393+
}
394+
395+
private Object readResolve() {
396+
if(StringUtils.isBlank(webhookSecretCredentialsId) && secretToken != null) {
397+
migrateWebhookSecretCredentials();
398+
}
399+
return this;
400+
}
401+
402+
/**
403+
* Migrate webhook secret token to Jenkins credentials
404+
*/
405+
private void migrateWebhookSecretCredentials() {
406+
final List<StringCredentials> credentials =
407+
CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList());
408+
for (final StringCredentials cred : credentials) {
409+
if (StringUtils.equals(secretToken.getPlainText(), Secret.toString(cred.getSecret()))) {
410+
// If a credential has the same secret, use it.
411+
webhookSecretCredentialsId = cred.getId();
412+
break;
413+
}
414+
}
415+
if (StringUtils.isBlank(webhookSecretCredentialsId)) {
416+
// If we couldn't find any existing credentials, create new credential
417+
final StringCredentials newCredentials =
418+
new StringCredentialsImpl(
419+
CredentialsScope.GLOBAL,
420+
null,
421+
"Migrated from gitlab-branch-source-plugin webhook secret",
422+
secretToken);
423+
SystemCredentialsProvider.getInstance().getCredentials().add(newCredentials);
424+
webhookSecretCredentialsId = newCredentials.getId();
425+
}
426+
secretToken = null;
319427
}
320428

321429
/**
@@ -485,13 +593,11 @@ public FormValidation doTestConnection(@QueryParameter String serverUrl,
485593
try {
486594
User user = gitLabApi.getUserApi().getCurrentUser();
487595
LOGGER.log(Level.FINEST, String
488-
.format("Connection established with the GitLab Server for %s",
489-
user.getUsername()));
596+
.format("Connection established with the GitLab Server for %s", user.getUsername()));
490597
return FormValidation
491598
.ok(String.format("Credentials verified for user %s", user.getUsername()));
492599
} catch (GitLabApiException e) {
493-
LOGGER.log(Level.SEVERE,
494-
"Failed to connect with GitLab Server - %s", e.getMessage());
600+
LOGGER.log(Level.SEVERE, "Failed to connect with GitLab Server - %s", e.getMessage());
495601
return FormValidation.error(e,
496602
Messages.GitLabServer_failedValidation(Util.escape(e.getMessage())));
497603
}
@@ -522,6 +628,30 @@ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String serverUrl,
522628
CREDENTIALS_MATCHER);
523629
}
524630

631+
/**
632+
* Stapler form completion.
633+
*
634+
* @param webhookSecretCredentialsId the webhook secret credentials Id
635+
* @return the available credentials.
636+
*/
637+
@Restricted(NoExternalUse.class) // stapler
638+
@SuppressWarnings("unused")
639+
public ListBoxModel doFillWebhookSecretCredentialsIdItems(@QueryParameter String serverUrl,
640+
@QueryParameter String webhookSecretCredentialsId) {
641+
Jenkins jenkins = Jenkins.get();
642+
if (!jenkins.hasPermission(Jenkins.ADMINISTER)) {
643+
return new StandardListBoxModel().includeCurrentValue(webhookSecretCredentialsId);
644+
}
645+
return new StandardListBoxModel()
646+
.includeEmptyValue()
647+
.includeMatchingAs(ACL.SYSTEM,
648+
jenkins,
649+
StringCredentials.class,
650+
fromUri(serverUrl).build(),
651+
WEBHOOK_SECRET_CREDENTIALS_MATCHER
652+
);
653+
}
654+
525655
private PersonalAccessToken getCredentials(String serverUrl, String credentialsId) {
526656
Jenkins jenkins = Jenkins.get();
527657
jenkins.checkPermission(Jenkins.ADMINISTER);

src/main/resources/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer/config.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ f.entry(title: _("System Hook"), field: "manageSystemHooks", "description": "Do
2828
f.checkbox(title: _("Manage System Hooks"))
2929
}
3030

31-
f.entry(title: _("Secret Token"), field: "secretToken", "description": "The secret token used while setting up hook url in the GitLab server") {
32-
f.password()
31+
f.entry(title: _("Secret Token"), field: "webhookSecretCredentialsId", "description": "The secret token used while setting up hook url in the GitLab server") {
32+
c.select(context: app)
3333
}
3434

3535
f.entry(title: _("Root URL for hooks"), field: "hooksRootUrl", "description": "Jenkins root URL to use in hooks URL (if different from the public Jenkins root URL)") {

src/main/resources/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer/help-secretToken.html

Lines changed: 0 additions & 3 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div>
2+
The secret token is required to authenticate the webhook payloads received from a GitLab Server. Use generate secret token from Advanced options or use your own. If you are an old plugin user and did not set a secret token previously and want the secret token to be applied to the hooks of your existing jobs, you can add the secret token and rescan your jobs. Existing hooks with new secret token will be applied.
3+
</div>

0 commit comments

Comments
 (0)