From bcd78419efe26e8222962ca356a41f4009be8502 Mon Sep 17 00:00:00 2001 From: Tomas Bjerre Date: Wed, 2 Sep 2015 18:21:05 +0200 Subject: [PATCH] Adding optional regular expression to injection feature #56 * To be able to extract crumb from Jenkins even if primitive XPath result sets forbidden --- CHANGELOG.md | 3 +++ README.md | 13 +++++++-- .../se/bjurr/prnfs/admin/AdminFormValues.java | 3 ++- .../bjurr/prnfs/listener/PrnfsRenderer.java | 17 ++++++++++-- .../prnfs/settings/PrnfsNotification.java | 27 ++++++++++++------- .../settings/PrnfsNotificationBuilder.java | 8 +++++- .../bjurr/prnfs/settings/SettingsStorage.java | 4 +++ src/main/resources/admin.vm | 1 + .../PrnfsPullRequestEventListenerTest.java | 26 ++++++++++++++++++ 9 files changed, 87 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aed8973..555f5dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Changelog of Pull Request Notifier for Stash. +## 1.27 +* Adding an optional regular expression, that can be evaluated in the response from injection URL, to populate ${INJECTION_URL_VALUE}. + ## 1.26 * Removing XPath alternative, introduced in version 1.22. It may not work in all installations. And is is not needed for Jenkins, which was the original use case. diff --git a/README.md b/README.md index cdc5ea6d..185f165c 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ The Pull Request Notifier for Stash can: * RESCOPED_FROM, when source branch change * RESCOPED_TO, when target branch change * BUTTON_TRIGGER, when trigger button in pull request view is pressed -* Can invoke CSRF protected systems, using the ${INJECTION_URL_VALUE} variable - * With Jenkins you set the Injection URL field to: `JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)` +* Can invoke CSRF protected systems, using the ${INJECTION_URL_VALUE} variable. How to to that with Jenkins is described below. * Be configured to only trigger if the pull request mathches a filter. A filter text is constructed with any combination of the variables and then a regexp is constructed to match that text. * Add buttons to pull request view in Stash. And map those buttons to URL invocations. This can be done by setting the filter string to ${BUTTON_TRIGGER_TITLE} and the filter regexp to title of button. * Authenticate with HTTP basic authentication. @@ -70,6 +69,16 @@ The ${PULL_REQUEST_USER...} contains information about the user who issued the e You may want to use [Violation Comments to Stash plugin](https://wiki.jenkins-ci.org/display/JENKINS/Violation+Comments+to+Stash+Plugin) and/or [StashNotifier plugin](https://wiki.jenkins-ci.org/display/JENKINS/StashNotifier+Plugin) to report results back to Stash. +### Jenkins +Parameterized Jenkins jobs can be triggered remotely via: +``` +http://server/job/theJob/buildWithParameters?token=TOKEN&PARAMETER=Value +``` + +If you are using a CSRF protection in Jenkins, you can use the **Injection URL** feature. +* Set **Injection URL** field to `http://JENKINS/crumbIssuer/api/xml?xpath=//crumb/text()`. You may get an error like *primitive XPath result sets forbidden; implement jenkins.security.SecureRequester*. If so, you can set Injection URL to `http://JENKINS/crumbIssuer/api/xml?xpath=//crumb` in combination with regular expression `([^<]*)`. Or a third option is to checkout [this](https://wiki.jenkins-ci.org/display/JENKINS/Secure+Requester+Whitelist+Plugin) Jenkins plugin. +* In the headers section, set header **.crumb** with value **${INJECTION_URL_VALUE}**. + ## Developer instructions Prerequisites: diff --git a/src/main/java/se/bjurr/prnfs/admin/AdminFormValues.java b/src/main/java/se/bjurr/prnfs/admin/AdminFormValues.java index 65409b3a..d28e291a 100644 --- a/src/main/java/se/bjurr/prnfs/admin/AdminFormValues.java +++ b/src/main/java/se/bjurr/prnfs/admin/AdminFormValues.java @@ -46,6 +46,7 @@ public enum FIELDS { button_visibility, // admin_allowed, // user_allowed, // - injection_url + injection_url, // + injection_url_regexp } } diff --git a/src/main/java/se/bjurr/prnfs/listener/PrnfsRenderer.java b/src/main/java/se/bjurr/prnfs/listener/PrnfsRenderer.java index 36110047..225bb84e 100644 --- a/src/main/java/se/bjurr/prnfs/listener/PrnfsRenderer.java +++ b/src/main/java/se/bjurr/prnfs/listener/PrnfsRenderer.java @@ -1,6 +1,7 @@ package se.bjurr.prnfs.listener; import static java.util.logging.Logger.getLogger; +import static java.util.regex.Pattern.compile; import static se.bjurr.prnfs.listener.PrnfsRenderer.REPO_PROTOCOL.http; import static se.bjurr.prnfs.listener.PrnfsRenderer.REPO_PROTOCOL.ssh; import static se.bjurr.prnfs.listener.UrlInvoker.urlInvoker; @@ -9,6 +10,7 @@ import java.util.Map; import java.util.Set; import java.util.logging.Logger; +import java.util.regex.Matcher; import se.bjurr.prnfs.settings.PrnfsNotification; @@ -304,12 +306,23 @@ public String resolve(PullRequest pullRequest, PrnfsPullRequestAction pullReques } UrlInvoker urlInvoker = urlInvoker() // .withUrlParam(prnfsNotification.getInjectionUrl().get()) // - .withMethod(GET).withProxyServer(prnfsNotification.getProxyServer()) // + .withMethod(GET)// + .withProxyServer(prnfsNotification.getProxyServer()) // .withProxyPort(prnfsNotification.getProxyPort()) // .withProxyUser(prnfsNotification.getProxyUser()) // .withProxyPassword(prnfsNotification.getProxyPassword()); PrnfsRenderer.invoker.invoke(urlInvoker); - return urlInvoker.getResponseString().trim(); + String rawResponse = urlInvoker.getResponseString().trim(); + if (prnfsNotification.getInjectionUrlRegexp().isPresent()) { + Matcher m = compile(prnfsNotification.getInjectionUrlRegexp().get()).matcher(rawResponse); + if (!m.find()) { + logger.severe("Could not find \"" + prnfsNotification.getInjectionUrlRegexp().get() + "\" in:\n" + rawResponse); + return ""; + } + return m.group(1); + } else { + return rawResponse; + } } }); diff --git a/src/main/java/se/bjurr/prnfs/settings/PrnfsNotification.java b/src/main/java/se/bjurr/prnfs/settings/PrnfsNotification.java index 17857869..e07ac6eb 100644 --- a/src/main/java/se/bjurr/prnfs/settings/PrnfsNotification.java +++ b/src/main/java/se/bjurr/prnfs/settings/PrnfsNotification.java @@ -9,6 +9,9 @@ import static java.util.regex.Pattern.compile; import static se.bjurr.prnfs.admin.AdminFormValues.DEFAULT_NAME; import static se.bjurr.prnfs.admin.AdminFormValues.VALUE; +import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.filter_regexp; +import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.filter_string; +import static se.bjurr.prnfs.admin.AdminFormValues.FORM_TYPE.TRIGGER_CONFIG_FORM; import static se.bjurr.prnfs.listener.UrlInvoker.HTTP_METHOD.GET; import static se.bjurr.prnfs.settings.PrnfsPredicates.predicate; @@ -17,6 +20,7 @@ import java.util.Map; import se.bjurr.prnfs.admin.AdminFormValues; +import se.bjurr.prnfs.admin.AdminFormValues.FIELDS; import se.bjurr.prnfs.admin.AdminFormValues.FORM_TYPE; import se.bjurr.prnfs.listener.PrnfsPullRequestAction; import se.bjurr.prnfs.listener.UrlInvoker.HTTP_METHOD; @@ -39,11 +43,12 @@ public class PrnfsNotification { private final Integer proxyPort; private final String name; private final String injectionUrl; + private final String injectionUrlRegexp; public PrnfsNotification(List triggers, String url, String user, String password, String filterString, String filterRegexp, String method, String postContent, List
headers, String proxyUser, - String proxyPassword, String proxyServer, String proxyPort, String name, String injectionUrl) - throws ValidationException { + String proxyPassword, String proxyServer, String proxyPort, String name, String injectionUrl, + String injectionUrlRegexp) throws ValidationException { this.proxyUser = emptyToNull(nullToEmpty(proxyUser).trim()); this.proxyPassword = emptyToNull(nullToEmpty(proxyPassword).trim()); this.proxyServer = emptyToNull(nullToEmpty(proxyServer).trim()); @@ -52,23 +57,22 @@ public PrnfsNotification(List triggers, String url, Stri this.postContent = emptyToNull(nullToEmpty(postContent).trim()); this.method = HTTP_METHOD.valueOf(firstNonNull(emptyToNull(nullToEmpty(method).trim()), GET.name())); if (nullToEmpty(url).trim().isEmpty()) { - throw new ValidationException(AdminFormValues.FIELDS.url.name(), "URL not set!"); + throw new ValidationException(FIELDS.url.name(), "URL not set!"); } try { new URL(url); } catch (final Exception e) { - throw new ValidationException(AdminFormValues.FIELDS.url.name(), "URL not valid!"); + throw new ValidationException(FIELDS.url.name(), "URL not valid!"); } if (!nullToEmpty(filterRegexp).trim().isEmpty()) { try { compile(filterRegexp); } catch (final Exception e) { - throw new ValidationException(AdminFormValues.FIELDS.filter_regexp.name(), "Filter regexp not valid! " + throw new ValidationException(filter_regexp.name(), "Filter regexp not valid! " + e.getMessage().replaceAll("\n", " ")); } if (nullToEmpty(filterString).trim().isEmpty()) { - throw new ValidationException(AdminFormValues.FIELDS.filter_string.name(), - "Filter string not set, nothing to match regexp against!"); + throw new ValidationException(filter_string.name(), "Filter string not set, nothing to match regexp against!"); } } this.url = url; @@ -79,6 +83,7 @@ public PrnfsNotification(List triggers, String url, Stri this.filterRegexp = filterRegexp; this.name = firstNonNull(emptyToNull(nullToEmpty(name).trim()), DEFAULT_NAME); this.injectionUrl = emptyToNull(nullToEmpty(injectionUrl).trim()); + this.injectionUrlRegexp = emptyToNull(nullToEmpty(injectionUrlRegexp).trim()); } public Optional getFilterRegexp() { @@ -139,11 +144,15 @@ public List
getHeaders() { public static boolean isOfType(AdminFormValues config, FORM_TYPE formType) { Optional> formTypeOpt = tryFind(config, predicate(AdminFormValues.FIELDS.FORM_TYPE.name())); - return !formTypeOpt.isPresent() && formType.name().equals(FORM_TYPE.TRIGGER_CONFIG_FORM.name()) - || formTypeOpt.get().get(VALUE).equals(AdminFormValues.FORM_TYPE.TRIGGER_CONFIG_FORM.name()); + return !formTypeOpt.isPresent() && formType.name().equals(TRIGGER_CONFIG_FORM.name()) + || formTypeOpt.get().get(VALUE).equals(TRIGGER_CONFIG_FORM.name()); } public Optional getInjectionUrl() { return fromNullable(injectionUrl); } + + public Optional getInjectionUrlRegexp() { + return fromNullable(injectionUrlRegexp); + } } diff --git a/src/main/java/se/bjurr/prnfs/settings/PrnfsNotificationBuilder.java b/src/main/java/se/bjurr/prnfs/settings/PrnfsNotificationBuilder.java index c201b87a..b58a7d1b 100644 --- a/src/main/java/se/bjurr/prnfs/settings/PrnfsNotificationBuilder.java +++ b/src/main/java/se/bjurr/prnfs/settings/PrnfsNotificationBuilder.java @@ -30,13 +30,14 @@ public static PrnfsNotificationBuilder prnfsNotificationBuilder() { private String proxyPort; private String name; private String injectionUrl; + private String injectionUrlRegexp; private PrnfsNotificationBuilder() { } public PrnfsNotification build() throws ValidationException { return new PrnfsNotification(triggers, url, user, password, filterString, filterRegexp, method, postContent, headers, - proxyUser, proxyPassword, proxyServer, proxyPort, name, injectionUrl); + proxyUser, proxyPassword, proxyServer, proxyPort, name, injectionUrl, injectionUrlRegexp); } public PrnfsNotificationBuilder withInjectionUrl(String injectionUrl) { @@ -44,6 +45,11 @@ public PrnfsNotificationBuilder withInjectionUrl(String injectionUrl) { return this; } + public PrnfsNotificationBuilder withInjectionUrlRegexp(String injectionUrlRegexp) { + this.injectionUrlRegexp = checkNotNull(injectionUrlRegexp); + return this; + } + public PrnfsNotificationBuilder withPassword(String password) { this.password = checkNotNull(password); return this; diff --git a/src/main/java/se/bjurr/prnfs/settings/SettingsStorage.java b/src/main/java/se/bjurr/prnfs/settings/SettingsStorage.java index f686609a..7a8166d2 100644 --- a/src/main/java/se/bjurr/prnfs/settings/SettingsStorage.java +++ b/src/main/java/se/bjurr/prnfs/settings/SettingsStorage.java @@ -164,6 +164,10 @@ public static PrnfsNotification getPrnfsNotification(AdminFormValues adminFormVa prnfsNotificationBuilder.withInjectionUrl(find(adminFormValues, predicate(AdminFormValues.FIELDS.injection_url.name())).get(VALUE)); } + if (tryFind(adminFormValues, predicate(AdminFormValues.FIELDS.injection_url_regexp.name())).isPresent()) { + prnfsNotificationBuilder.withInjectionUrlRegexp(find(adminFormValues, + predicate(AdminFormValues.FIELDS.injection_url_regexp.name())).get(VALUE)); + } return prnfsNotificationBuilder.build(); } diff --git a/src/main/resources/admin.vm b/src/main/resources/admin.vm index 60de6b43..23ace267 100644 --- a/src/main/resources/admin.vm +++ b/src/main/resources/admin.vm @@ -168,6 +168,7 @@
Injection URL (Optional) +

The (GET) response from this URL will be available in the ${INJECTION_URL_VALUE} variable.

diff --git a/src/test/java/se/bjurr/prnfs/admin/PrnfsPullRequestEventListenerTest.java b/src/test/java/se/bjurr/prnfs/admin/PrnfsPullRequestEventListenerTest.java index 42a337f5..bb9dfc42 100644 --- a/src/test/java/se/bjurr/prnfs/admin/PrnfsPullRequestEventListenerTest.java +++ b/src/test/java/se/bjurr/prnfs/admin/PrnfsPullRequestEventListenerTest.java @@ -23,6 +23,7 @@ import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.header_name; import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.header_value; import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.injection_url; +import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.injection_url_regexp; import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.method; import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.password; import static se.bjurr.prnfs.admin.AdminFormValues.FIELDS.post_content; @@ -1611,6 +1612,31 @@ public void testThatValueFromUrlCanBeUsedInInvocation() throws Exception { .invokedOnlyUrl("http://bjurr.se/?some%20content"); } + @Test + public void testThatValueFromUrlAndRegexpCanBeUsedInInvocation() throws Exception { + prnfsTestBuilder() // + .isLoggedInAsAdmin() // + .withNotification( // + notificationBuilder() // + .withFieldValue(url, "http://bjurr.se/?${" + INJECTION_URL_VALUE + "}") // + .withFieldValue(events, OPENED.name()) // + .withFieldValue(FORM_TYPE, TRIGGER_CONFIG_FORM.name()) // + .withFieldValue(injection_url, "http://bjurr.se/get") // + .withFieldValue(injection_url_regexp, "([^<]*)") // + .build() // + ) // + .store() // + .withResponse( + "http://bjurr.se/get", + " \n 986a2d65b7987258434da5583c9f5337.crumb \n ") // + .trigger( // + pullRequestEventBuilder() // + .withPullRequestAction(OPENED) // + .build() // + ) // + .invokedOnlyUrl("http://bjurr.se/?986a2d65b7987258434da5583c9f5337"); + } + @Test public void testThatProxyMayNotBeUsedWhenInvokingUrl() throws Exception { prnfsTestBuilder()//