2 | Request that the submitter specify one or more parameter values when approving.
3 | If just one parameter is listed, its value will become the value of the input step.
4 | If multiple parameters are listed, the return value will be a map keyed by the parameter names.
5 | If parameters are not requested, the step returns nothing if approved.
6 |
7 |
8 | On the parameter entry screen you are able to enter values for parameters that are defined in this field.
9 |
2 | User IDs and/or external group names of person or people permitted to respond to the input, separated by ','.
3 | Spaces will be trimmed. This means that "alice, bob, blah " is the same as "alice,bob,blah".
4 | Note: Jenkins administrators are able to respond to the input regardless of the value of this parameter. Users with Job/cancel permission may also respond with 'Abort' to the input.
5 |
2 | If specified, this is the name of the return value that will contain the ID of the user that approves this
3 | input.
4 |
5 | The return value will be handled in a fashion similar to the parameters value.
6 |
3 | This step pauses Pipeline execution and allows the user to interact and control the flow of the build.
4 | Only a basic "proceed" or "abort" option is provided in the stage view.
5 |
6 |
7 | You can optionally request information back, hence the name of the step.
8 | The parameter entry screen can be accessed via a link at the bottom of the build console log or
9 | via link in the sidebar for a build.
10 |
14 | Support for FileParameters will be removed in a future release.
15 | Details on how to migrate your pipeline can be found
16 | online.
17 |
18 |
19 |
20 |
21 |
22 |
23 | The input is using a URL unsafe id ("${it.id}").
24 | Ids should be restricted to characters that are URL safe that do not need encoding such as ASCII alpha numeric and a limited set of punctuation.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/Messages.properties:
--------------------------------------------------------------------------------
1 | paused_for_input=Paused for Input
2 | pipeline_need_input=Pipeline has paused and needs your input before proceeding
3 | wait_for_interactive_input=Wait for interactive input
4 | abort=Abort
5 | rejected=Rejected
6 | rejected_by=Rejected by {0}
7 | input_submitted=Input Submitted
8 | proceed=Proceed
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepConfigTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2015 Jesse Glick.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package org.jenkinsci.plugins.workflow.support.steps.input;
26 |
27 | import java.util.Collections;
28 |
29 | import org.jenkinsci.plugins.structs.describable.DescribableModel;
30 | import org.jenkinsci.plugins.workflow.steps.StepConfigTester;
31 | import org.junit.Test;
32 | import static org.junit.Assert.*;
33 | import org.junit.Rule;
34 | import org.jvnet.hudson.test.Issue;
35 | import org.jvnet.hudson.test.JenkinsRule;
36 |
37 | public class InputStepConfigTest {
38 |
39 | @Rule public JenkinsRule r = new JenkinsRule();
40 |
41 | @Test public void configRoundTrip() throws Exception {
42 | InputStep s1 = new InputStep("hello world");
43 | InputStep s2 = new StepConfigTester(r).configRoundTrip(s1);
44 | assertEquals(s1.getMessage(), s2.getMessage());
45 | assertEquals(s1.getId(), s2.getId());
46 | assertEquals(s1.getParameters(), s2.getParameters());
47 | assertEquals(s1.getOk(), s2.getOk());
48 | assertEquals(s1.getSubmitter(), s2.getSubmitter());
49 | }
50 |
51 | @Issue("JENKINS-25779")
52 | @Test public void uninstantiate() throws Exception {
53 | InputStep s = new InputStep("hello world");
54 | assertEquals(Collections.singletonMap("message", s.getMessage()), DescribableModel.uninstantiate_(s));
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepRestartTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2014 Jesse Glick.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package org.jenkinsci.plugins.workflow.support.steps.input;
26 |
27 | import hudson.model.Executor;
28 | import hudson.model.Result;
29 | import java.io.File;
30 | import java.util.ArrayList;
31 | import java.util.List;
32 | import org.apache.commons.io.FileUtils;
33 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
34 | import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker;
35 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
36 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
37 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
38 | import org.jenkinsci.plugins.workflow.support.actions.PauseAction;
39 |
40 | import static org.awaitility.Awaitility.await;
41 | import static org.hamcrest.Matchers.notNullValue;
42 | import static org.junit.Assert.*;
43 | import org.junit.ClassRule;
44 | import org.junit.Rule;
45 | import org.junit.Test;
46 | import org.jvnet.hudson.test.BuildWatcher;
47 | import org.jvnet.hudson.test.Issue;
48 | import org.jvnet.hudson.test.JenkinsRule;
49 | import org.jvnet.hudson.test.JenkinsSessionRule;
50 |
51 | public class InputStepRestartTest {
52 |
53 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
54 | @Rule public JenkinsSessionRule sessions = new JenkinsSessionRule();
55 |
56 | @Issue("JENKINS-25889")
57 | @Test public void restart() throws Throwable {
58 | sessions.then(j -> {
59 | WorkflowJob p = j.createProject(WorkflowJob.class, "p");
60 | p.setDefinition(new CpsFlowDefinition("input 'paused'", true));
61 | WorkflowRun b = p.scheduleBuild2(0).waitForStart();
62 | j.waitForMessage("paused", b);
63 | });
64 | sessions.then(j -> {
65 | WorkflowRun b = j.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1);
66 | proceed(b, j);
67 | j.assertBuildStatusSuccess(j.waitForCompletion(b));
68 | sanity(b);
69 | });
70 | }
71 |
72 | private static void proceed(WorkflowRun b, JenkinsRule j) throws Exception {
73 | InputAction a = b.getAction(InputAction.class);
74 | assertNotNull(a);
75 | assertEquals(1, a.getExecutions().size());
76 | j.submit(j.createWebClient().getPage(b, a.getUrlName()).getFormByName(a.getExecutions().get(0).getId()), "proceed");
77 | }
78 |
79 | private void sanity(WorkflowRun b) throws Exception {
80 | List pauses = new ArrayList<>();
81 | for (FlowNode n : new FlowGraphWalker(b.getExecution())) {
82 | pauses.addAll(PauseAction.getPauseActions(n));
83 | }
84 | assertEquals(1, pauses.size());
85 | assertFalse(pauses.get(0).isPaused());
86 | String xml = FileUtils.readFileToString(new File(b.getRootDir(), "build.xml"));
87 | assertFalse(xml, xml.contains(InputStepExecution.class.getName()));
88 | }
89 |
90 | @Issue("JENKINS-37154")
91 | @Test public void interrupt() throws Throwable {
92 | sessions.then(j -> {
93 | WorkflowJob p = j.createProject(WorkflowJob.class, "p");
94 | p.setDefinition(new CpsFlowDefinition("catchError {input 'paused'}", true));
95 | WorkflowRun b = p.scheduleBuild2(0).waitForStart();
96 | j.waitForMessage("paused", b);
97 | });
98 | sessions.then(j -> {
99 | WorkflowRun b = j.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1);
100 | assertNotNull(b);
101 | assertTrue(b.isBuilding());
102 | Executor executor = await().until(b::getExecutor, notNullValue());
103 | executor.interrupt();
104 | j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(b));
105 | sanity(b);
106 | });
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013-2014, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package org.jenkinsci.plugins.workflow.support.steps.input;
26 |
27 | import com.cloudbees.plugins.credentials.CredentialsParameterDefinition;
28 | import com.cloudbees.plugins.credentials.CredentialsParameterValue;
29 | import com.cloudbees.plugins.credentials.CredentialsProvider;
30 | import com.cloudbees.plugins.credentials.CredentialsScope;
31 | import com.cloudbees.plugins.credentials.domains.Domain;
32 | import org.htmlunit.ElementNotFoundException;
33 | import org.htmlunit.HttpMethod;
34 | import org.htmlunit.WebRequest;
35 | import org.htmlunit.html.HtmlAnchor;
36 | import org.htmlunit.html.HtmlElementUtil;
37 | import org.htmlunit.html.HtmlFileInput;
38 | import org.htmlunit.html.HtmlForm;
39 | import org.htmlunit.html.HtmlPage;
40 | import com.google.common.base.Predicate;
41 | import hudson.model.BooleanParameterDefinition;
42 | import hudson.model.Cause;
43 | import hudson.model.CauseAction;
44 | import hudson.model.Job;
45 | import hudson.model.ParametersAction;
46 | import hudson.model.ParametersDefinitionProperty;
47 | import hudson.model.Result;
48 | import hudson.model.User;
49 | import hudson.model.queue.QueueTaskFuture;
50 |
51 |
52 | import java.io.File;
53 | import java.io.IOException;
54 |
55 | import hudson.security.ACL;
56 | import hudson.security.ACLContext;
57 | import hudson.util.FormValidation.Kind;
58 | import hudson.util.Secret;
59 | import jenkins.model.IdStrategy;
60 | import jenkins.model.Jenkins;
61 | import net.sf.json.JSONArray;
62 | import net.sf.json.JSONObject;
63 | import org.apache.commons.lang.StringUtils;
64 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
65 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
66 | import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
67 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
68 | import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
69 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
70 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
71 | import org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty;
72 | import org.junit.ClassRule;
73 | import org.junit.Rule;
74 | import org.junit.Test;
75 | import org.jvnet.hudson.test.BuildWatcher;
76 | import org.jvnet.hudson.test.FlagRule;
77 | import org.jvnet.hudson.test.Issue;
78 | import org.jvnet.hudson.test.JenkinsMatchers;
79 | import org.jvnet.hudson.test.JenkinsRule;
80 |
81 | import java.util.Arrays;
82 | import java.util.Map;
83 | import java.util.Optional;
84 | import java.util.UUID;
85 |
86 | import org.jvnet.hudson.test.MockAuthorizationStrategy;
87 | import org.jvnet.hudson.test.WithoutJenkins;
88 | import edu.umd.cs.findbugs.annotations.Nullable;
89 | import org.jvnet.hudson.test.recipes.LocalData;
90 |
91 | import static org.hamcrest.MatcherAssert.assertThat;
92 | import static org.hamcrest.Matchers.allOf;
93 | import static org.hamcrest.Matchers.arrayContaining;
94 | import static org.hamcrest.Matchers.containsString;
95 | import static org.hamcrest.Matchers.not;
96 | import static org.junit.Assert.assertEquals;
97 | import static org.junit.Assert.assertFalse;
98 | import static org.junit.Assert.assertTrue;
99 | import static org.junit.Assert.assertNotNull;
100 |
101 | /**
102 | * @author Kohsuke Kawaguchi
103 | */
104 | public class InputStepTest {
105 | @Rule public JenkinsRule j = new JenkinsRule();
106 |
107 | @ClassRule
108 | public static BuildWatcher buildWatcher = new BuildWatcher();
109 |
110 | @Rule public FlagRule allowUnsafeParams = FlagRule.systemProperty(InputStepExecution.UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME, null);
111 |
112 | /**
113 | * Try out a parameter.
114 | */
115 | @Test
116 | public void parameter() throws Exception {
117 |
118 |
119 | //set up dummy security real
120 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
121 | // job setup
122 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
123 | foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList(
124 | "echo('before');",
125 | "def x = input message:'Do you want chocolate?', id:'Icecream', ok: 'Purchase icecream', parameters: [[$class: 'BooleanParameterDefinition', name: 'chocolate', defaultValue: false, description: 'Favorite icecream flavor']], submitter:'alice';",
126 | "echo(\"after: ${x}\");"),"\n"),true));
127 |
128 |
129 | // get the build going, and wait until workflow pauses
130 | QueueTaskFuture q = foo.scheduleBuild2(0);
131 | WorkflowRun b = q.getStartCondition().get();
132 | CpsFlowExecution e = (CpsFlowExecution) b.getExecutionPromise().get();
133 |
134 | while (b.getAction(InputAction.class)==null) {
135 | e.waitForSuspension();
136 | }
137 |
138 | // make sure we are pausing at the right state that reflects what we wrote in the program
139 | InputAction a = b.getAction(InputAction.class);
140 | assertEquals(1, a.getExecutions().size());
141 |
142 | InputStepExecution is = a.getExecution("Icecream");
143 | assertEquals("Do you want chocolate?", is.getInput().getMessage());
144 | assertEquals(1, is.getInput().getParameters().size());
145 | assertEquals("alice", is.getInput().getSubmitter());
146 |
147 | j.assertEqualDataBoundBeans(is.getInput().getParameters().get(0), new BooleanParameterDefinition("chocolate", false, "Favorite icecream flavor"));
148 |
149 | // submit the input, and run workflow to the completion
150 | JenkinsRule.WebClient wc = j.createWebClient();
151 | wc.login("alice");
152 | HtmlPage p = wc.getPage(b, a.getUrlName());
153 | j.submit(p.getFormByName(is.getId()), "proceed");
154 | assertEquals(0, a.getExecutions().size());
155 | q.get();
156 |
157 | // make sure the valid hyperlink of the approver is created in the build index page
158 | HtmlAnchor pu =null;
159 |
160 | try {
161 | pu = p.getAnchorByText("alice");
162 | }
163 | catch(ElementNotFoundException ex){
164 | System.out.println("valid hyperlink of the approved does not appears on the build index page");
165 | }
166 |
167 | assertNotNull(pu);
168 |
169 | // make sure 'x' gets assigned to false
170 |
171 | j.assertLogContains("after: false", b);
172 |
173 | //make sure the approver name corresponds to the submitter
174 | ApproverAction action = b.getAction(ApproverAction.class);
175 | assertNotNull(action);
176 | assertEquals("alice", action.getUserId());
177 |
178 | DepthFirstScanner scanner = new DepthFirstScanner();
179 |
180 | FlowNode nodeWithInputSubmittedAction = scanner.findFirstMatch(e.getCurrentHeads(), null, new Predicate() {
181 | @Override
182 | public boolean apply(@Nullable FlowNode input) {
183 | return input != null && input.getAction(InputSubmittedAction.class) != null;
184 | }
185 | });
186 | assertNotNull(nodeWithInputSubmittedAction);
187 | InputSubmittedAction inputSubmittedAction = nodeWithInputSubmittedAction.getAction(InputSubmittedAction.class);
188 | assertNotNull(inputSubmittedAction);
189 |
190 | assertEquals("alice", inputSubmittedAction.getApprover());
191 | Map submittedParams = inputSubmittedAction.getParameters();
192 | assertEquals(1, submittedParams.size());
193 | assertTrue(submittedParams.containsKey("chocolate"));
194 | assertEquals(false, submittedParams.get("chocolate"));
195 | }
196 |
197 | @Test
198 | @Issue("JENKINS-26363")
199 | public void test_cancel_run_by_input() throws Exception {
200 | JenkinsRule.WebClient webClient = j.createWebClient();
201 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
202 | j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
203 | // Only give "alice" and "bob" basic privs. That's normally not enough to Job.CANCEL, only for the fact that "alice"
204 | // and "bob" are listed as the submitter.
205 | grant(Jenkins.READ, Job.READ).everywhere().to("alice", "bob").
206 | // Give "charlie" basic privs + Job.CANCEL. That should allow user3 cancel.
207 | grant(Jenkins.READ, Job.READ, Job.CANCEL).everywhere().to("charlie"));
208 |
209 | final WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
210 | foo.setDefinition(new CpsFlowDefinition("input id: 'InputX', message: 'OK?', cancel: 'No', ok: 'Yes', submitter: 'alice'", true));
211 |
212 | runAndAbort(webClient, foo, "alice", true); // alice should work coz she's declared as 'submitter'
213 | runAndAbort(webClient, foo, "bob", false); // bob shouldn't work coz he's not declared as 'submitter' and doesn't have Job.CANCEL privs
214 | runAndAbort(webClient, foo, "charlie", true); // charlie should work coz he has Job.CANCEL privs
215 | }
216 |
217 | @Test
218 | @Issue("SECURITY-576")
219 | public void needBuildPermission() throws Exception {
220 | JenkinsRule.WebClient webClient = j.createWebClient();
221 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
222 | j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
223 | // Only give "alice" basic privs. She can not proceed since she doesn't have build permissions.
224 | grant(Jenkins.READ, Job.READ).everywhere().to("alice").
225 | // Give "bob" basic privs + Job.BUILD. That should allow bob proceed.
226 | grant(Jenkins.READ, Job.READ, Job.BUILD).everywhere().to("bob"));
227 |
228 | final WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
229 | foo.setDefinition(new CpsFlowDefinition("input id: 'InputX', message: 'OK?', cancel: 'No', ok: 'Yes'", true));
230 |
231 | // alice should not work coz she doesn't have Job.BUILD privs
232 | runAndContinue(webClient, foo, "alice", false);
233 |
234 | // bob should work coz he has Job.BUILD privs.
235 | runAndContinue(webClient, foo, "bob", true);
236 | }
237 |
238 | @Test
239 | @Issue("JENKINS-31425")
240 | public void test_submitters() throws Exception {
241 | JenkinsRule.WebClient webClient = j.createWebClient();
242 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
243 | j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
244 | // Only give "alice" basic privs. That's normally not enough to Job.CANCEL, only for the fact that "alice"
245 | // is listed as the submitter.
246 | grant(Jenkins.READ, Job.READ).everywhere().to("alice").
247 | // Only give "bob" basic privs. That's normally not enough to Job.CANCEL, only for the fact that "bob"
248 | // is listed as the submitter.
249 | grant(Jenkins.READ, Job.READ).everywhere().to("bob").
250 | // Give "charlie" basic privs. That's normally not enough to Job.CANCEL, and isn't listed as submiter.
251 | grant(Jenkins.READ, Job.READ).everywhere().to("charlie").
252 | // Add an admin user that should be able to approve the job regardless)
253 | grant(Jenkins.ADMINISTER).everywhere().to("admin"));
254 |
255 | final WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
256 | foo.setDefinition(new CpsFlowDefinition("input id: 'InputX', message: 'OK?', cancel: 'No', ok: 'Yes', submitter: 'alice,BoB'", true));
257 |
258 | runAndAbort(webClient, foo, "alice", true); // alice should work coz she's declared as 'submitter'
259 | assertEquals(IdStrategy.CASE_INSENSITIVE, j.jenkins.getSecurityRealm().getUserIdStrategy());
260 | runAndAbort(webClient, foo, "bob", true); // bob should work coz he's declared as 'submitter'
261 | runAndContinue(webClient, foo, "bob", true); // bob should work coz he's declared as 'submitter'
262 | runAndAbort(webClient, foo, "charlie", false); // charlie shouldn't work coz he's not declared as 'submitter' and doesn't have Job.CANCEL privs
263 | runAndContinue(webClient, foo, "admin", true); // admin should work because... they can do anything
264 | }
265 |
266 | @Test
267 | @Issue({"JENKINS-31396","JENKINS-40594"})
268 | public void test_submitter_parameter() throws Exception {
269 | //set up dummy security real
270 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
271 | // job setup
272 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
273 | foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList(
274 | "def x = input message:'Do you want chocolate?', id:'Icecream', ok: 'Purchase icecream', submitter:'alice,bob', submitterParameter: 'approval';",
275 | "echo(\"after: ${x}\");"),"\n"),true));
276 |
277 | // get the build going, and wait until workflow pauses
278 | QueueTaskFuture q = foo.scheduleBuild2(0);
279 | WorkflowRun b = q.getStartCondition().get();
280 | j.waitForMessage("Input requested", b);
281 |
282 | // make sure we are pausing at the right state that reflects what we wrote in the program
283 | InputAction a = b.getAction(InputAction.class);
284 | assertEquals(1, a.getExecutions().size());
285 |
286 | InputStepExecution is = a.getExecution("Icecream");
287 | assertEquals("Do you want chocolate?", is.getInput().getMessage());
288 | assertEquals("alice,bob", is.getInput().getSubmitter());
289 |
290 | // submit the input, and run workflow to the completion
291 | JenkinsRule.WebClient wc = j.createWebClient();
292 | wc.login("alice");
293 | HtmlPage console_page = wc.getPage(b, "console");
294 | assertFalse(console_page.asXml().contains("proceedEmpty"));
295 | HtmlPage p = wc.getPage(b, a.getUrlName());
296 | j.submit(p.getFormByName(is.getId()), "proceed");
297 | assertEquals(0, a.getExecutions().size());
298 | q.get();
299 |
300 | // make sure 'x' gets 'alice'
301 | j.assertLogContains("after: alice", b);
302 | }
303 |
304 | @Test
305 | @Issue("JENKINS-31396")
306 | public void test_submitter_parameter_no_submitter() throws Exception {
307 | //set up dummy security real
308 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
309 | // job setup
310 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
311 | foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList(
312 | "def x = input message:'Do you want chocolate?', id:'Icecream', ok: 'Purchase icecream', submitterParameter: 'approval';",
313 | "echo(\"after: ${x}\");"),"\n"),true));
314 |
315 | // get the build going, and wait until workflow pauses
316 | QueueTaskFuture q = foo.scheduleBuild2(0);
317 | WorkflowRun b = q.getStartCondition().get();
318 | j.waitForMessage("Input requested", b);
319 |
320 | // make sure we are pausing at the right state that reflects what we wrote in the program
321 | InputAction a = b.getAction(InputAction.class);
322 | assertEquals(1, a.getExecutions().size());
323 |
324 | InputStepExecution is = a.getExecution("Icecream");
325 | assertEquals("Do you want chocolate?", is.getInput().getMessage());
326 |
327 | // submit the input, and run workflow to the completion
328 | JenkinsRule.WebClient wc = j.createWebClient();
329 | wc.login("alice");
330 | HtmlPage p = wc.getPage(b, a.getUrlName());
331 | j.submit(p.getFormByName(is.getId()), "proceed");
332 | assertEquals(0, a.getExecutions().size());
333 | q.get();
334 |
335 | // make sure 'x' gets 'alice'
336 | j.assertLogContains("after: alice", b);
337 | }
338 |
339 | private void runAndAbort(JenkinsRule.WebClient webClient, WorkflowJob foo, String loginAs, boolean expectAbortOk) throws Exception {
340 | // get the build going, and wait until workflow pauses
341 | QueueTaskFuture queueTaskFuture = foo.scheduleBuild2(0);
342 | WorkflowRun run = queueTaskFuture.getStartCondition().get();
343 | CpsFlowExecution execution = (CpsFlowExecution) run.getExecutionPromise().get();
344 |
345 | while (run.getAction(InputAction.class) == null) {
346 | execution.waitForSuspension();
347 | }
348 |
349 | webClient.login(loginAs);
350 |
351 | InputAction inputAction = run.getAction(InputAction.class);
352 | InputStepExecution is = inputAction.getExecution("InputX");
353 | HtmlPage p = webClient.getPage(run, inputAction.getUrlName());
354 |
355 | try {
356 | j.submit(p.getFormByName(is.getId()), "abort");
357 | assertEquals(0, inputAction.getExecutions().size());
358 | queueTaskFuture.get();
359 |
360 | assertTrue(expectAbortOk);
361 | j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(run));
362 | } catch (Exception e) {
363 | assertFalse(expectAbortOk);
364 | j.waitForMessage("Yes or No", run);
365 | run.doStop();
366 | j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(run));
367 | }
368 | }
369 |
370 | private void runAndContinue(JenkinsRule.WebClient webClient, WorkflowJob foo, String loginAs, boolean expectContinueOk) throws Exception {
371 | // get the build going, and wait until workflow pauses
372 | QueueTaskFuture queueTaskFuture = foo.scheduleBuild2(0);
373 | WorkflowRun run = queueTaskFuture.getStartCondition().get();
374 | CpsFlowExecution execution = (CpsFlowExecution) run.getExecutionPromise().get();
375 |
376 | while (run.getAction(InputAction.class) == null) {
377 | execution.waitForSuspension();
378 | }
379 |
380 | webClient.login(loginAs);
381 |
382 | InputAction inputAction = run.getAction(InputAction.class);
383 | InputStepExecution is = inputAction.getExecution("InputX");
384 | HtmlPage p = webClient.getPage(run, inputAction.getUrlName());
385 |
386 | try {
387 | j.submit(p.getFormByName(is.getId()), "proceed");
388 | assertEquals(0, inputAction.getExecutions().size());
389 | queueTaskFuture.get();
390 |
391 | assertTrue(expectContinueOk);
392 | j.assertBuildStatusSuccess(j.waitForCompletion(run)); // Should be successful.
393 | } catch (Exception e) {
394 | assertFalse(expectContinueOk);
395 | j.waitForMessage("Yes or No", run);
396 | run.doStop();
397 | j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(run));
398 | }
399 | }
400 |
401 | @Test
402 | public void abortPreviousBuilds() throws Exception {
403 | //Create a new job and set the AbortPreviousBuildsJobProperty
404 | WorkflowJob job = j.createProject(WorkflowJob.class, "myJob");
405 | job.setDefinition(new CpsFlowDefinition("input 'proceed?'", true));
406 | DisableConcurrentBuildsJobProperty jobProperty = new DisableConcurrentBuildsJobProperty();
407 | jobProperty.setAbortPrevious(true);
408 | job.addProperty(jobProperty);
409 | job.save();
410 |
411 | //Run the job and wait for the input step
412 | WorkflowRun run1 = job.scheduleBuild2(0).waitForStart();
413 | j.waitForMessage("proceed", run1);
414 |
415 | //run another job and wait for the input step
416 | WorkflowRun run2 = job.scheduleBuild2(0).waitForStart();
417 | j.waitForMessage("proceed", run2);
418 |
419 | //check that the first job has been aborted with the result of NOT_BUILT
420 | j.assertBuildStatus(Result.NOT_BUILT, j.waitForCompletion(run1));
421 | }
422 |
423 | @Issue("JENKINS-38380")
424 | @Test public void timeoutAuth() throws Exception {
425 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
426 | j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("ops"));
427 | WorkflowJob p = j.createProject(WorkflowJob.class, "p");
428 | p.setDefinition(new CpsFlowDefinition("timeout(time: 1, unit: 'SECONDS') {input message: 'OK?', submitter: 'ops'}", true));
429 | j.assertBuildStatus(Result.ABORTED, p.scheduleBuild2(0).get());
430 | }
431 |
432 | @Issue("JENKINS-47699")
433 | @Test
434 | public void userScopedCredentials() throws Exception {
435 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
436 | final User alpha = User.getById("alpha", true);
437 | final String alphaSecret = "correct horse battery staple";
438 | final String alphaId = registerUserSecret(alpha, alphaSecret);
439 | final User beta = User.getOrCreateByIdOrFullName("beta");
440 | final String betaSecret = "hello world bad password";
441 | final String betaId = registerUserSecret(beta, betaSecret);
442 | final User gamma = User.getOrCreateByIdOrFullName("gamma");
443 | final String gammaSecret = "proton mass decay string";
444 | final String gammaId = registerUserSecret(gamma, gammaSecret);
445 | final User delta = User.getOrCreateByIdOrFullName("delta");
446 | final String deltaSecret = "fundamental arithmetic theorem prover";
447 | final String deltaId = registerUserSecret(delta, deltaSecret);
448 |
449 | final WorkflowJob p = j.createProject(WorkflowJob.class);
450 | p.addProperty(new ParametersDefinitionProperty(
451 | new CredentialsParameterDefinition("deltaId", null, null, StringCredentialsImpl.class.getName(), true)
452 | ));
453 | p.setDefinition(new CpsFlowDefinition("node {\n" +
454 | stringCredentialsInput("AlphaCreds", "alphaId") +
455 | stringCredentialsInput("BetaCreds", "betaId") +
456 | stringCredentialsInput("GammaCreds", "gammaId") +
457 | " withCredentials([\n" +
458 | " string(credentialsId: 'alphaId', variable: 'alphaSecret'),\n" +
459 | " string(credentialsId: 'betaId', variable: 'betaSecret'),\n" +
460 | " string(credentialsId: 'gammaId', variable: 'gammaSecret'),\n" +
461 | " string(credentialsId: 'deltaId', variable: 'deltaSecret')\n" +
462 | " ]) {\n" +
463 | " if (alphaSecret != '" + alphaSecret + "') {\n" +
464 | " error 'invalid alpha credentials'\n" +
465 | " }\n" +
466 | " if (betaSecret != '" + betaSecret + "') {\n" +
467 | " error 'invalid beta credentials'\n" +
468 | " }\n" +
469 | " if (gammaSecret != '" + gammaSecret + "') {\n" +
470 | " error 'invalid gamma credentials'\n" +
471 | " }\n" +
472 | " if (deltaSecret != '" + deltaSecret + "') {\n" +
473 | " error 'invalid delta credentials'\n" +
474 | " }\n" +
475 | " }\n" +
476 | "}", true));
477 |
478 | // schedule a parameterized build
479 | final QueueTaskFuture runFuture;
480 | try (ACLContext ignored = ACL.as(delta)) {
481 | runFuture = p.scheduleBuild2(0,
482 | new CauseAction(new Cause.UserIdCause()),
483 | new ParametersAction(new CredentialsParameterValue("deltaId", deltaId, null))
484 | );
485 | assertNotNull(runFuture);
486 | }
487 | final JenkinsRule.WebClient wc = j.createWebClient();
488 | final WorkflowRun run = runFuture.waitForStart();
489 | final CpsFlowExecution execution = (CpsFlowExecution) run.getExecutionPromise().get();
490 |
491 | selectUserCredentials(wc, run, execution, alphaId, "alpha", "AlphaCreds");
492 | selectUserCredentials(wc, run, execution, betaId, "beta", "BetaCreds");
493 | selectUserCredentials(wc, run, execution, gammaId, "gamma", "GammaCreds");
494 |
495 | j.assertBuildStatusSuccess(runFuture);
496 | }
497 |
498 | @Issue("JENKINS-63516")
499 | @Test
500 | public void passwordParameters() throws Exception {
501 | WorkflowJob p = j.createProject(WorkflowJob.class);
502 | p.setDefinition(new CpsFlowDefinition(
503 | "def password = input(message: 'Proceed?', id: 'MyId', parameters: [\n" +
504 | " password(name: 'myPassword', defaultValue: 'mySecret', description: 'myDescription')\n" +
505 | "])\n" +
506 | "echo('Password is ' + password)", true));
507 | WorkflowRun b = p.scheduleBuild2(0).waitForStart();
508 | while (b.getAction(InputAction.class) == null) {
509 | Thread.sleep(100);
510 | }
511 | InputAction action = b.getAction(InputAction.class);
512 | assertEquals(1, action.getExecutions().size());
513 | JenkinsRule.WebClient wc = j.createWebClient();
514 | HtmlPage page = wc.getPage(b, action.getUrlName());
515 | j.submit(page.getFormByName(action.getExecution("MyId").getId()), "proceed");
516 | j.assertBuildStatusSuccess(j.waitForCompletion(b));
517 | j.assertLogContains("Password is mySecret", b);
518 | }
519 |
520 | @Issue("SECURITY-2705")
521 | @Test
522 | public void fileParameterWithEscapeHatch() throws Exception {
523 | System.setProperty(InputStepExecution.UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME, "true");
524 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
525 | foo.setDefinition(new CpsFlowDefinition("node {\n" +
526 | "input message: 'Please provide a file', parameters: [file('paco.txt')], id: 'Id' \n" +
527 | " }",true));
528 |
529 | // get the build going, and wait until workflow pauses
530 | QueueTaskFuture q = foo.scheduleBuild2(0);
531 | WorkflowRun b = q.waitForStart();
532 | j.waitForMessage("Input requested", b);
533 |
534 | InputAction action = b.getAction(InputAction.class);
535 | assertEquals(1, action.getExecutions().size());
536 |
537 | // submit the input, and expect a failure, no need to set any file value as the check we are testing takes
538 | // place before we try to interact with the file
539 | JenkinsRule.WebClient wc = j.createWebClient();
540 | HtmlPage p = wc.getPage(b, action.getUrlName());
541 | HtmlForm f = p.getFormByName("Id");
542 | HtmlFileInput fileInput = f.getInputByName("file");
543 | fileInput.setValue("dummy.txt");
544 | fileInput.setContentType("text/csv");
545 | String currentTime = "Current time " + System.currentTimeMillis();
546 | fileInput.setData(currentTime.getBytes());
547 | j.submit(f, "proceed");
548 |
549 | j.assertBuildStatus(Result.SUCCESS, j.waitForCompletion(b));
550 | assertTrue(new File(b.getRootDir(), "paco.txt").exists());
551 | assertThat(JenkinsRule.getLog(b),
552 | allOf(containsString(InputStepExecution.UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME),
553 | containsString("will be removed in a future release"),
554 | containsString("https://jenkins.io/redirect/plugin/pipeline-input-step/file-parameters")));
555 | }
556 |
557 | @Issue("SECURITY-2705")
558 | @Test
559 | public void fileParameterShouldFailAtRuntime() throws Exception {
560 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
561 | foo.setDefinition(new CpsFlowDefinition("input message: 'Please provide a file', parameters: [file('paco.txt')], id: 'Id'",true));
562 |
563 | // get the build going, and wait until workflow pauses
564 | QueueTaskFuture q = foo.scheduleBuild2(0);
565 | WorkflowRun b = q.waitForStart();
566 |
567 | j.assertBuildStatus(Result.FAILURE, j.waitForCompletion(b));
568 | assertThat(JenkinsRule.getLog(b),
569 | allOf(not(containsString(InputStepExecution.UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME)),
570 | containsString("https://jenkins.io/redirect/plugin/pipeline-input-step/file-parameters")));
571 | }
572 |
573 | @LocalData
574 | @Test public void serialForm() throws Exception {
575 | WorkflowJob p = j.jenkins.getItemByFullName("p", WorkflowJob.class);
576 | WorkflowRun b = p.getBuildByNumber(1);
577 | JenkinsRule.WebClient wc = j.createWebClient();
578 | wc.getPage(new WebRequest(wc.createCrumbedUrl("job/p/1/input/9edfbbe09847e1bfee4f8d2b0abfd1c3/proceedEmpty"), HttpMethod.POST));
579 | j.assertBuildStatusSuccess(j.waitForCompletion(b));
580 | }
581 |
582 | private void selectUserCredentials(JenkinsRule.WebClient wc, WorkflowRun run, CpsFlowExecution execution, String credentialsId, String username, String inputId) throws Exception {
583 | while (run.getAction(InputAction.class) == null) {
584 | execution.waitForSuspension();
585 | }
586 | wc.login(username);
587 | final InputAction action = run.getAction(InputAction.class);
588 | final HtmlForm form = wc.getPage(run, action.getUrlName()).getFormByName(action.getExecution(inputId).getId());
589 | HtmlElementUtil.click(form.getInputByName("includeUser"));
590 | form.getSelectByName("_.value").setSelectedAttribute(credentialsId, true);
591 | j.submit(form, "proceed");
592 | }
593 |
594 | private static String registerUserSecret(User user, String value) throws IOException {
595 | try (ACLContext ignored = ACL.as(user)) {
596 | final String credentialsId = UUID.randomUUID().toString();
597 | CredentialsProvider.lookupStores(user).iterator().next().addCredentials(Domain.global(),
598 | new StringCredentialsImpl(CredentialsScope.USER, credentialsId, null, Secret.fromString(value)));
599 | return credentialsId;
600 | }
601 | }
602 |
603 | private static String stringCredentialsInput(String id, String name) {
604 | return "input id: '" + id + "', message: '', parameters: [credentials(credentialType: 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl', defaultValue: '', description: '', name: '" + name + "', required: true)]\n";
605 | }
606 |
607 | @Test
608 | @Issue("SECURITY-2880")
609 | public void test_unsafe_ids_are_rejected() throws Exception {
610 | WorkflowJob wf = j.jenkins.createProject(WorkflowJob.class, "foo");
611 | wf.setDefinition(new CpsFlowDefinition("input message:'wait', id:'../&escape Me'", true));
612 | // get the build going, and wait until workflow pauses
613 | j.buildAndAssertStatus(Result.FAILURE, wf);
614 | }
615 |
616 | @Test
617 | @WithoutJenkins
618 | @Issue("SECURITY-2880")
619 | public void test_unsafe_ids_generate_formValidation() throws Exception {
620 | InputStep.DescriptorImpl d = new InputStep.DescriptorImpl();
621 | assertThat("simple dash separated strings should be allowed", d.doCheckId("this-is-ok"), JenkinsMatchers.hasKind(Kind.OK));
622 | assertThat("something more complex with safe characters should be allowed", d.doCheckId("this-is~*_(ok)!"), JenkinsMatchers.hasKind(Kind.OK));
623 |
624 | assertThat("dot should be rejected", d.doCheckId("."), JenkinsMatchers.hasKind(Kind.ERROR));
625 | assertThat("dot dot should be rejected", d.doCheckId(".."), JenkinsMatchers.hasKind(Kind.ERROR));
626 | assertThat("foo.bar should be allowed", d.doCheckId("foo.bar"), JenkinsMatchers.hasKind(Kind.OK));
627 |
628 | assertThat("ampersands should be rejected", d.doCheckId("this-is-&-not-ok"), JenkinsMatchers.hasKind(Kind.ERROR));
629 | assertThat("% should be rejected", d.doCheckId("a-%-should-fail"), JenkinsMatchers.hasKind(Kind.ERROR));
630 | assertThat("# should be rejected", d.doCheckId("a-#-should-fail"), JenkinsMatchers.hasKind(Kind.ERROR));
631 | assertThat("' should be rejected", d.doCheckId("a-single-quote-should-fail'"), JenkinsMatchers.hasKind(Kind.ERROR));
632 | assertThat("\" should be rejected", d.doCheckId("a-single-quote-should-fail\""), JenkinsMatchers.hasKind(Kind.ERROR));
633 | assertThat("/ should be rejected", d.doCheckId("/this-is-also-not-ok"), JenkinsMatchers.hasKind(Kind.ERROR));
634 | assertThat("< should be rejected", d.doCheckId("this-is- should be rejected", d.doCheckId("this-is-also>-not-ok"), JenkinsMatchers.hasKind(Kind.ERROR));
636 | }
637 |
638 | @Test
639 | public void test_api_contains_waitingForInput() throws Exception {
640 | //set up dummy security real
641 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
642 | // job setup
643 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
644 | foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList(
645 | "def x = input message:'Continue?';",
646 | "echo(\"after: ${x}\");"),"\n"),true));
647 |
648 | // get the build going, and wait until workflow pauses
649 | QueueTaskFuture q = foo.scheduleBuild2(0);
650 | WorkflowRun b = q.getStartCondition().get();
651 | j.waitForMessage("Continue?", b);
652 |
653 | final JenkinsRule.WebClient webClient = j.createWebClient();
654 | JenkinsRule.JSONWebResponse json = webClient.getJSON(b.getUrl() + "api/json?depth=1");
655 | JSONArray actions = json.getJSONObject().getJSONArray("actions");
656 | Optional