├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── CHANGELOG.md ├── Jenkinsfile ├── README.md ├── docs └── fileParameters.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── workflow │ │ └── support │ │ └── steps │ │ └── input │ │ ├── ApproverAction.java │ │ ├── InputAction.java │ │ ├── InputStep.java │ │ ├── InputStepExecution.java │ │ ├── InputSubmittedAction.java │ │ ├── Outcome.java │ │ ├── Rejection.java │ │ └── package-info.java └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── workflow │ └── support │ └── steps │ └── input │ ├── ApproverAction │ ├── summary.jelly │ └── summary.properties │ ├── InputAction │ └── index.jelly │ ├── InputStep │ ├── config.jelly │ ├── help-cancel.html │ ├── help-id.html │ ├── help-message.html │ ├── help-ok.html │ ├── help-parameters.html │ ├── help-submitter.html │ ├── help-submitterParameter.html │ └── help.html │ ├── InputStepExecution │ └── index.jelly │ └── Messages.properties └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── workflow │ └── support │ └── steps │ └── input │ ├── InputStepConfigTest.java │ ├── InputStepRestartTest.java │ └── InputStepTest.java └── resources └── org └── jenkinsci └── plugins └── workflow └── support └── steps └── input └── InputStepTest └── serialForm ├── jobs └── p │ ├── builds │ ├── 1 │ │ ├── build.xml │ │ ├── log │ │ ├── log-index │ │ ├── program.dat │ │ └── workflow │ │ │ ├── 2.xml │ │ │ └── 3.xml │ ├── legacyIds │ └── permalinks │ ├── config.xml │ └── nextBuildNumber └── org.jenkinsci.plugins.workflow.flow.FlowExecutionList.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/pipeline-input-step-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | *.iml 4 | .idea 5 | /.classpath 6 | /.project 7 | /.settings/ 8 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - For newer versions, see [GitHub Releases](https://github.com/jenkinsci/pipeline-input-step-plugin/releases) 4 | 5 | ## 2.12 6 | 7 | Released 2020-08-28 8 | 9 | - Fix: Make password parameters work with the `input` step in Jenkins 2.236 and newer ([JENKINS-63516](https://issues.jenkins-ci.org/browse/JENKINS-63516)) 10 | - Improvement: Document that Jenkins administrators are always able to approve `input` steps ([JENKINS-56016](https://issues.jenkins-ci.org/browse/JENKINS-56016)) 11 | - Improvement: Migrate documentation from Wiki to GitHub ([PR 43](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/43)) 12 | - Internal: Update parent POM and dependencies ([PR 38](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/38), [PR 40](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/40), [PR 42](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/42)) 13 | - Internal: Fix flaky test ([PR 45](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/45)) 14 | 15 | ## 2.11 16 | 17 | Released 2019-08-27 18 | 19 | - [JENKINS-47699](https://issues.jenkins-ci.org/browse/JENKINS-47699): 20 | Allow user-scope credentials to be used as `input` step parameters. 21 | - Internal: Replace uses of deprecated APIs ([PR 22 | 37](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/37)) 23 | 24 | ## 2.10 25 | 26 | Released 2019-03-18 27 | 28 | - Trim submitter names before making comparisons to avoid issues with 29 | whitespace ([PR 30 | 30](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/30)) 31 | - Add internationalization support ([PR 32 | 23](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/23)) 33 | - Internal: Update dependencies and fix resulting test failures so 34 | that the plugin's tests pass successfully when run using the PCT 35 | ([PR 36 | 31](https://github.com/jenkinsci/pipeline-input-step-plugin/pull/31)) 37 | 38 | ## 2.9 39 | 40 | Released 2018-12-14 41 | 42 | - [JENKINS-55181](https://issues.jenkins-ci.org/browse/JENKINS-55181) 43 | Compare the ID of the user attempting to submit an input step 44 | against the list of valid submitters using the IdStrategy and 45 | GroupIdStrategy configured by the current security realm. 46 | Previously, these comparisons were always case-sensitive. 47 | 48 | ## 2.8 49 | 50 | Released 2017-08-07 51 | 52 | - [Fix security issue](https://jenkins.io/security/advisory/2017-08-07/) 53 | 54 | ## 2.7 55 | 56 | Released 2017-04-26 57 | 58 | - [JENKINS-43856](https://issues.jenkins-ci.org/browse/JENKINS-43856) `NoSuchMethodError` 59 | from Blue Ocean code after the change in 2.6. 60 | 61 | - [JENKINS-40594](https://issues.jenkins-ci.org/browse/JENKINS-40594) The `submitterParameter` 62 | option added in 2.4 did not show the correct kind of hyperlink in 63 | the console unless `parameters` was also specified. 64 | 65 | ## 2.6 66 | 67 | Released 2017-04-24 68 | 69 | - [JENKINS-40926](https://issues.jenkins-ci.org/browse/JENKINS-40926) API 70 | to retrieve parameters of a particular submission. No user-visible 71 | change. 72 | 73 | ## 2.5 74 | 75 | Released 2016-11-09 76 | 77 | - 2.4 was corrupt. 78 | 79 | ## 2.4 80 | 81 | Released 2016-11-09 82 | 83 | **Corrupt, use 2.5 instead** 84 | 85 | - [JENKINS-31425](https://issues.jenkins-ci.org/browse/JENKINS-31425) 86 | The `submitter` parameter now accepts a comma-separated list. 87 | - [JENKINS-31396](https://issues.jenkins-ci.org/browse/JENKINS-31396) 88 | `submitterParameter` parameter added. 89 | - [JENKINS-38380](https://issues.jenkins-ci.org/browse/JENKINS-38380) 90 | Interrupting an `input` step, for example with `timeout`, did not 91 | generally work in a secured system. 92 | 93 | ## 2.3 94 | 95 | Released 2016-10-21 96 | 97 | - [JENKINS-39168](https://issues.jenkins-ci.org/browse/JENKINS-39168) 98 | Exception thrown under some conditions from fix in 2.2. 99 | 100 | ## 2.2 101 | 102 | Released 2016-10-20 103 | 104 | - More 105 | [JENKINS-37154](https://issues.jenkins-ci.org/browse/JENKINS-37154) 106 | tuning timeouts 107 | 108 | ## 2.1 109 | 110 | Released 2016-08-08 111 | 112 | - [JENKINS-37154](https://issues.jenkins-ci.org/browse/JENKINS-37154) 113 | Deadlock under some conditions when aborting a build waiting in 114 | `input`. 115 | 116 | ## 2.0 117 | 118 | Released 2016-04-05 119 | 120 | - First release under per-plugin versioning scheme. See [1.x 121 | changelog](https://github.com/jenkinsci/workflow-plugin/blob/82e7defa37c05c5f004f1ba01c93df61ea7868a5/CHANGES.md) 122 | for earlier releases. 123 | - Includes the `input` step formerly in [Pipeline Supporting APIs Plugin](https://plugins.jenkins.io/workflow-support/). 124 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | configurations: [ 4 | [platform: 'linux', jdk: 21], 5 | [platform: 'windows', jdk: 17], 6 | ]) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipeline: Input Step Plugin 2 | 3 | Adds the Pipeline `input` step to wait for human input or approval. 4 | A basic Proceed or Abort option is provided in the stage view. 5 | 6 | The parameter entry screen can be accessed via a link at the bottom of the build console log or via link in the sidebar for a build. 7 | 8 | Input Example with `Message Parameter`: 9 | 10 | input 'Proceed or Abort' 11 | 12 | 13 | Input Example with `Custom identifier`: 14 | Every input step has an unique identifier. It is used in the generated URL to proceed or abort. 15 | 16 | A specific identifier could be used, for example, to mechanically respond to the input from some external process/tool. 17 | 18 | input id: '2', message: 'input-message' 19 | 20 | 21 | Input Example with `Cancel Button Caption` and `OK Button Caption` (Optional): 22 | 23 | input message: 'input-message', cancel: 'Cancel', ok: 'OK' 24 | 25 | Input Example with `Allowed Submitter`: 26 | Usernames and/or external group names of those permitted to respond to the input, separated by ','. Spaces will be trimmed automatically, so "Alice, Bob, Charles" is the same as "Alice,Bob,Charles". 27 | Note: Jenkins administrators are able to respond to the input regardless of the value of this parameter. Users with [**Job/cancel** permission](https://www.jenkins.io/doc/book/security/access-control/permissions/#job-permissions) may also respond with 'Abort' to the input. 28 | 29 | input message: '', submitter: 'jenkins-user, jenkins-user2' 30 | 31 | Input Example with Parameter to store the `Approving Submitter`: 32 | 33 | If specified, this is the name of the return value that will contain the username of the user that approves this input. The return value will be handled in a fashion similar to the parameters value. 34 | 35 | input message: 'input-message', submitter: 'jenkins-submitter, jenkins-submitter2', submitterParameter: 'approvers-id-to-be-stored' 36 | 37 | 38 | Use the [Pipeline Syntax Snippet Generator](https://www.jenkins.io/redirect/pipeline-snippet-generator) to select options for the `input` step. 39 | For further understanding, check the [pipeline input step plugin documentation](https://www.jenkins.io/doc/pipeline/steps/pipeline-input-step/). 40 | 41 | 42 | 43 | ## Version History 44 | Please refer to [the changelog](CHANGELOG.md) 45 | -------------------------------------------------------------------------------- /docs/fileParameters.md: -------------------------------------------------------------------------------- 1 | # File Parameters 2 | 3 | Prior to `449.v77f0e8b_845c4` the `input` step allowed the specification of all parameters including `FileParametersValue`s. 4 | This was both flawed (it caused errors persisting the build, and only worked on the controller thus required builds to run on the controller which is insecure), as well as being insecure in itself (potentially allowing overwriting of arbitrary files). 5 | 6 | As the support is now been disabled if you were using this idiom before, you now need to update your pipelines to continue working. 7 | 8 | ## Migration 9 | 10 | To migrate we suggest the [File Parameter plugin](https://plugins.jenkins.io/file-parameters/). 11 | 12 | Where a pipeline was before doing something similar to the following: 13 | 14 | ```groovy 15 | def file = input message: 'Please provide a file', parameters: [file('myFile.txt')] 16 | node('built-in') { 17 | // do something with the file stored in $file 18 | } 19 | ``` 20 | 21 | it can be changed to use the following syntax 22 | 23 | ```groovy 24 | def fileBase64 = input message: 'Please provide a file', parameters: [base64File('file')] 25 | node { 26 | withEnv(["fileBase64=$fileBase64"]) { 27 | sh 'echo $fileBase64 | base64 -d > myFile.txt' 28 | // powershell '[IO.File]::WriteAllBytes("myFile.txt", [Convert]::FromBase64String($env:fileBase64))' 29 | } 30 | // do something with the file stored in ./myFile.txt 31 | } 32 | ``` 33 | 34 | Please see the [File Parameter plugin documentaion](https://github.com/jenkinsci/file-parameters-plugin#usage-with-input) for more details 35 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 5.9 8 | 9 | 10 | pipeline-input-step 11 | ${changelist} 12 | hpi 13 | Pipeline: Input Step 14 | https://github.com/jenkinsci/${project.artifactId}-plugin 15 | 16 | 17 | MIT License 18 | https://opensource.org/licenses/MIT 19 | 20 | 21 | 22 | scm:git:https://github.com/${gitHubRepo}.git 23 | scm:git:git@github.com:${gitHubRepo}.git 24 | https://github.com/${gitHubRepo} 25 | ${scmTag} 26 | 27 | 28 | 29 | repo.jenkins-ci.org 30 | https://repo.jenkins-ci.org/public/ 31 | 32 | 33 | 34 | 35 | repo.jenkins-ci.org 36 | https://repo.jenkins-ci.org/public/ 37 | 38 | 39 | 40 | 999999-SNAPSHOT 41 | 42 | 2.479 43 | ${jenkins.baseline}.1 44 | jenkinsci/${project.artifactId}-plugin 45 | 46 | 47 | 48 | 49 | io.jenkins.tools.bom 50 | bom-${jenkins.baseline}.x 51 | 3893.v213a_42768d35 52 | pom 53 | import 54 | 55 | 56 | org.awaitility 57 | awaitility 58 | 4.3.0 59 | 60 | 61 | 62 | 63 | 64 | org.jenkins-ci.plugins 65 | structs 66 | 67 | 68 | org.jenkins-ci.plugins.workflow 69 | workflow-step-api 70 | 71 | 72 | org.jenkins-ci.plugins.workflow 73 | workflow-api 74 | 75 | 76 | org.jenkins-ci.plugins.workflow 77 | workflow-support 78 | 79 | 80 | org.jenkins-ci.plugins 81 | credentials 82 | 83 | 84 | org.jenkins-ci.plugins.workflow 85 | workflow-cps 86 | test 87 | 88 | 89 | org.jenkins-ci.plugins.workflow 90 | workflow-job 91 | test 92 | 93 | 94 | org.jenkins-ci.plugins.workflow 95 | workflow-basic-steps 96 | test 97 | 98 | 99 | org.jenkins-ci.plugins.workflow 100 | workflow-durable-task-step 101 | test 102 | 103 | 104 | org.jenkins-ci.plugins.workflow 105 | workflow-step-api 106 | tests 107 | test 108 | 109 | 110 | org.jenkins-ci.plugins.workflow 111 | workflow-scm-step 112 | test 113 | 114 | 115 | org.jenkins-ci.plugins 116 | script-security 117 | test 118 | 119 | 120 | org.jenkins-ci.plugins 121 | credentials-binding 122 | test 123 | 124 | 125 | org.awaitility 126 | awaitility 127 | test 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/ApproverAction.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 hudson.model.InvisibleAction; 28 | import hudson.model.User; 29 | import org.kohsuke.accmod.Restricted; 30 | import org.kohsuke.accmod.restrictions.DoNotUse; 31 | import org.kohsuke.stapler.export.Exported; 32 | import org.kohsuke.stapler.export.ExportedBean; 33 | 34 | import edu.umd.cs.findbugs.annotations.NonNull; 35 | 36 | /** 37 | * @author Valentina Armenise 38 | */ 39 | 40 | @ExportedBean 41 | public class ApproverAction extends InvisibleAction { 42 | 43 | public ApproverAction(String userId) { 44 | this.userId = userId; 45 | } 46 | 47 | @NonNull 48 | final private String userId; 49 | 50 | @Exported 51 | public String getUserId() { 52 | return userId; 53 | } 54 | 55 | @Restricted(DoNotUse.class) 56 | public String getUserName() { 57 | return User.get(userId).getDisplayName(); 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.input; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.model.Run; 5 | import jenkins.model.RunAction2; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.TimeoutException; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | import edu.umd.cs.findbugs.annotations.NonNull; 17 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 18 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 19 | import org.kohsuke.stapler.export.Exported; 20 | import org.kohsuke.stapler.export.ExportedBean; 21 | 22 | /** 23 | * Records the pending inputs required. 24 | */ 25 | @ExportedBean 26 | public class InputAction implements RunAction2 { 27 | 28 | private static final Logger LOGGER = Logger.getLogger(InputAction.class.getName()); 29 | 30 | /** JENKINS-37154: number of seconds to block in {@link #loadExecutions} before we give up */ 31 | @SuppressWarnings("FieldMayBeFinal") 32 | private static /* not final */ int LOAD_EXECUTIONS_TIMEOUT = Integer.getInteger(InputAction.class.getName() + ".LOAD_EXECUTIONS_TIMEOUT", 60); 33 | 34 | private transient List executions = new ArrayList(); 35 | @SuppressFBWarnings(value="IS2_INCONSISTENT_SYNC", justification="CopyOnWriteArrayList") 36 | private List ids = new CopyOnWriteArrayList(); 37 | 38 | private transient Run run; 39 | 40 | @Override 41 | public void onAttached(Run r) { 42 | this.run = r; 43 | } 44 | 45 | @Override 46 | public void onLoad(Run r) { 47 | this.run = r; 48 | synchronized (this) { 49 | if (ids == null) { 50 | // Loading from before JENKINS-25889 fix. Load the IDs and discard the executions, which lack state anyway. 51 | assert executions != null && !executions.contains(null) : executions; 52 | ids = new ArrayList(); 53 | for (InputStepExecution execution : executions) { 54 | ids.add(execution.getId()); 55 | } 56 | executions = null; 57 | } 58 | } 59 | } 60 | 61 | private synchronized void loadExecutions() throws InterruptedException, TimeoutException { 62 | if (executions == null) { 63 | try { 64 | if (run instanceof FlowExecutionOwner.Executable) { 65 | var feo = ((FlowExecutionOwner.Executable) run).asFlowExecutionOwner(); 66 | if (feo != null) { 67 | var candidateExecutions = feo.get().getCurrentExecutions(true).get(LOAD_EXECUTIONS_TIMEOUT, TimeUnit.SECONDS); 68 | executions = new ArrayList<>(); // only set this if we know the answer 69 | // JENKINS-37154 sometimes we must block here in order to get accurate results 70 | for (StepExecution se : candidateExecutions) { 71 | if (se instanceof InputStepExecution) { 72 | InputStepExecution ise = (InputStepExecution) se; 73 | if (ids.contains(ise.getId())) { 74 | executions.add(ise); 75 | } 76 | } 77 | } 78 | if (executions.size() < ids.size()) { 79 | LOGGER.log(Level.WARNING, "some input IDs not restored from {0}", run); 80 | } 81 | } else { 82 | LOGGER.warning(() -> "no FlowExecutionOwner obtainable from " + run); 83 | } 84 | } else { 85 | LOGGER.warning(() -> "unrecognized build type " + run); 86 | } 87 | } catch (InterruptedException | TimeoutException x) { 88 | throw x; 89 | } catch (Exception x) { 90 | LOGGER.log(Level.WARNING, null, x); 91 | } 92 | } 93 | } 94 | 95 | public Run getRun() { 96 | return run; 97 | } 98 | 99 | @Override 100 | public String getIconFileName() { 101 | if (ids == null || ids.isEmpty()) { 102 | return null; 103 | } else { 104 | return "help.png"; 105 | } 106 | } 107 | 108 | @Exported 109 | @Override 110 | public String getDisplayName() { 111 | if (ids == null || ids.isEmpty()) { 112 | return null; 113 | } else { 114 | return Messages.paused_for_input(); 115 | } 116 | } 117 | 118 | @Override 119 | public String getUrlName() { 120 | return "input"; 121 | } 122 | 123 | public synchronized void add(@NonNull InputStepExecution step) throws IOException, InterruptedException, TimeoutException { 124 | loadExecutions(); 125 | if (executions == null) { 126 | throw new IOException("cannot load state"); 127 | } 128 | this.executions.add(step); 129 | ids.add(step.getId()); 130 | run.save(); 131 | } 132 | 133 | public synchronized InputStepExecution getExecution(String id) throws InterruptedException, TimeoutException { 134 | loadExecutions(); 135 | if (executions == null) { 136 | return null; 137 | } 138 | for (InputStepExecution e : executions) { 139 | if (e.input.getId().equals(id)) 140 | return e; 141 | } 142 | return null; 143 | } 144 | 145 | @Exported 146 | public synchronized List getExecutions() throws InterruptedException, TimeoutException { 147 | loadExecutions(); 148 | if (executions == null) { 149 | return Collections.emptyList(); 150 | } 151 | return new ArrayList(executions); 152 | } 153 | 154 | @Exported 155 | public boolean isWaitingForInput() throws InterruptedException, TimeoutException { 156 | return !getExecutions().isEmpty(); 157 | } 158 | 159 | /** 160 | * Called when {@link InputStepExecution} is completed to remove it from the active input list. 161 | */ 162 | public synchronized void remove(InputStepExecution exec) throws IOException, InterruptedException, TimeoutException { 163 | loadExecutions(); 164 | if (executions == null) { 165 | throw new IOException("cannot load state"); 166 | } 167 | executions.remove(exec); 168 | ids.remove(exec.getId()); 169 | run.save(); 170 | } 171 | 172 | /** 173 | * Bind steps just by their ID names. 174 | */ 175 | public InputStepExecution getDynamic(String token) throws InterruptedException, TimeoutException { 176 | return getExecution(token); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.input; 2 | 3 | import hudson.AbortException; 4 | import hudson.Extension; 5 | import hudson.ExtensionList; 6 | import hudson.Util; 7 | import hudson.model.FileParameterDefinition; 8 | import hudson.model.ParameterDefinition; 9 | import hudson.model.PasswordParameterDefinition; 10 | import hudson.model.Run; 11 | import hudson.model.TaskListener; 12 | import hudson.model.ParameterDefinition.ParameterDescriptor; 13 | import hudson.util.FormValidation; 14 | import hudson.util.FormValidation.Kind; 15 | import hudson.util.Secret; 16 | import java.io.Serializable; 17 | import java.util.Collections; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | import java.util.TreeMap; 23 | import java.util.function.Function; 24 | import java.util.stream.Collectors; 25 | import jenkins.model.Jenkins; 26 | import jenkins.util.SystemProperties; 27 | import org.acegisecurity.Authentication; 28 | import org.acegisecurity.GrantedAuthority; 29 | import org.jenkinsci.plugins.structs.describable.CustomDescribableModel; 30 | import org.jenkinsci.plugins.structs.describable.DescribableModel; 31 | import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; 32 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 33 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; 34 | import org.jenkinsci.plugins.workflow.steps.Step; 35 | import org.jenkinsci.plugins.workflow.steps.StepContext; 36 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 37 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 38 | import org.kohsuke.accmod.Restricted; 39 | import org.kohsuke.accmod.restrictions.NoExternalUse; 40 | import org.kohsuke.stapler.DataBoundConstructor; 41 | import org.kohsuke.stapler.DataBoundSetter; 42 | import org.kohsuke.stapler.QueryParameter; 43 | import org.kohsuke.stapler.export.Exported; 44 | import org.kohsuke.stapler.export.ExportedBean; 45 | 46 | /** 47 | * {@link Step} that pauses for human input. 48 | * 49 | * @author Kohsuke Kawaguchi 50 | */ 51 | @ExportedBean(defaultVisibility = 2) 52 | public class InputStep extends AbstractStepImpl implements Serializable { 53 | 54 | private static final boolean ALLOW_POTENTIALLY_UNSAFE_IDS = SystemProperties.getBoolean(InputStep.class.getName() + ".ALLOW_UNSAFE_IDS"); 55 | 56 | private final String message; 57 | 58 | /** 59 | * Optional ID that uniquely identifies this input from all others. 60 | */ 61 | private String id; 62 | 63 | /** 64 | * Optional user/group name who can approve this. 65 | */ 66 | private String submitter; 67 | 68 | /** 69 | * Optional parameter name to stored the user who responded to the input. 70 | */ 71 | private String submitterParameter; 72 | 73 | 74 | /** 75 | * Either a single {@link ParameterDefinition} or a list of them. 76 | */ 77 | private List parameters = Collections.emptyList(); 78 | 79 | /** 80 | * Caption of the Cancel button. 81 | */ 82 | private String cancel; 83 | 84 | /** 85 | * Caption of the OK button. 86 | */ 87 | private String ok; 88 | 89 | @DataBoundConstructor 90 | public InputStep(String message) { 91 | super(true); 92 | if (message==null) 93 | message = "Pipeline has paused and needs your input before proceeding"; 94 | this.message = message; 95 | } 96 | 97 | @DataBoundSetter 98 | public void setId(String id) { 99 | String _id = capitalize(Util.fixEmpty(id)); 100 | if (isIdConsideredUnsafe(_id)) { 101 | throw new IllegalArgumentException("InputStep id is required to be URL safe, but the provided id " + _id +" is not safe"); 102 | } 103 | this.id = _id; 104 | } 105 | 106 | @Exported 107 | public String getId() { 108 | if (id==null) 109 | id = capitalize(Util.getDigestOf(message)); 110 | return id; 111 | } 112 | 113 | @Exported 114 | public String getSubmitter() { 115 | return submitter; 116 | } 117 | 118 | @DataBoundSetter public void setSubmitter(String submitter) { 119 | this.submitter = Util.fixEmptyAndTrim(submitter); 120 | } 121 | 122 | @Exported 123 | public String getSubmitterParameter() { return submitterParameter; } 124 | 125 | @DataBoundSetter public void setSubmitterParameter(String submitterParameter) { 126 | this.submitterParameter = Util.fixEmptyAndTrim(submitterParameter); 127 | } 128 | 129 | private String capitalize(String id) { 130 | if (id==null) 131 | return null; 132 | if (id.length()==0) 133 | throw new IllegalArgumentException(); 134 | // a-z as the first char is reserved for InputAction 135 | char ch = id.charAt(0); 136 | if ('a'<=ch && ch<='z') 137 | id = ((char)(ch-'a'+'A')) + id.substring(1); 138 | return id; 139 | } 140 | 141 | /** 142 | * Caption of the Cancel button. 143 | */ 144 | @Exported 145 | public String getCancel() { 146 | return cancel!=null ? cancel : Messages.abort(); 147 | } 148 | 149 | @DataBoundSetter public void setCancel(String cancel) { 150 | this.cancel = Util.fixEmptyAndTrim(cancel); 151 | } 152 | 153 | /** 154 | * Caption of the OK button. 155 | */ 156 | @Exported 157 | public String getOk() { 158 | return ok!=null ? ok : Messages.proceed(); 159 | } 160 | 161 | @DataBoundSetter public void setOk(String ok) { 162 | this.ok = Util.fixEmptyAndTrim(ok); 163 | } 164 | 165 | @Exported 166 | public List getParameters() { 167 | return parameters; 168 | } 169 | 170 | @DataBoundSetter public void setParameters(List parameters) { 171 | this.parameters = parameters; 172 | } 173 | 174 | @Exported 175 | public String getMessage() { 176 | return message; 177 | } 178 | 179 | @Deprecated 180 | public boolean canSubmit() { 181 | Authentication a = Jenkins.getAuthentication(); 182 | return canSettle(a); 183 | } 184 | 185 | /** 186 | * Checks if the given user can settle this input. 187 | */ 188 | @Deprecated 189 | public boolean canSettle(Authentication a) { 190 | if (submitter==null) 191 | return true; 192 | final Set submitters = new HashSet<>(); 193 | Collections.addAll(submitters, submitter.split(",")); 194 | if (submitters.contains(a.getName())) 195 | return true; 196 | for (GrantedAuthority ga : a.getAuthorities()) { 197 | if (submitters.contains(ga.getAuthority())) 198 | return true; 199 | } 200 | return false; 201 | } 202 | 203 | @Override public StepExecution start(StepContext context) throws Exception { 204 | return new InputStepExecution(this, context); 205 | } 206 | 207 | 208 | @Override 209 | public DescriptorImpl getDescriptor() { 210 | return (DescriptorImpl)super.getDescriptor(); 211 | } 212 | 213 | /** 214 | * check if potentialId is considered unsafe for use as an id. 215 | * Even if it is unsafe this returns {@code false} if {@link #ALLOW_POTENTIALLY_UNSAFE_IDS} is {@code true} 216 | * @return {@code true} iff the id is unsafe and the escape hatch is not set 217 | */ 218 | private boolean isIdConsideredUnsafe(String potentialId) { 219 | if (ALLOW_POTENTIALLY_UNSAFE_IDS) { 220 | /// it is still unsafe 221 | return false; 222 | } 223 | return !getDescriptor().doCheckId(potentialId).kind.equals(Kind.OK); 224 | } 225 | 226 | private Object readResolve() throws AbortException { 227 | if (isIdConsideredUnsafe(this.id)) { 228 | throw new AbortException("InputStep id is required to be URL safe, but the provided id " + this.id +" is not safe"); 229 | } 230 | return this; 231 | } 232 | 233 | @Extension 234 | public static class DescriptorImpl extends StepDescriptor implements CustomDescribableModel { 235 | 236 | @Override 237 | public String getFunctionName() { 238 | return "input"; 239 | } 240 | 241 | @Override 242 | public String getDisplayName() { 243 | return Messages.wait_for_interactive_input(); 244 | } 245 | 246 | @Override public Set> getRequiredContext() { 247 | Set> context = new HashSet<>(); 248 | Collections.addAll(context, Run.class, TaskListener.class, FlowNode.class); 249 | return Collections.unmodifiableSet(context); 250 | } 251 | 252 | /** 253 | * Compatibility hack for JENKINS-63516. 254 | */ 255 | @Override 256 | public Map customInstantiate(Map map) { 257 | if (DescribableModel.of(PasswordParameterDefinition.class).getParameter("defaultValue") != null) { 258 | return map; 259 | } 260 | return copyMapReplacingEntry(map, "parameters", "parameters", List.class, parameters -> parameters.stream() 261 | .map(parameter -> { 262 | if (parameter instanceof UninstantiatedDescribable) { 263 | UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter; 264 | if (null != ud.getSymbol() && ud.getSymbol().equals("password")) { 265 | Map newArguments = copyMapReplacingEntry(ud.getArguments(), "defaultValue", "defaultValueAsSecret", String.class, Secret::fromString); 266 | return ud.withArguments(newArguments); 267 | } 268 | } 269 | return parameter; 270 | }) 271 | .collect(Collectors.toList()) 272 | ); 273 | } 274 | 275 | /** 276 | * Compatibility hack for JENKINS-63516. 277 | */ 278 | @Override 279 | public UninstantiatedDescribable customUninstantiate(UninstantiatedDescribable step) { 280 | if (DescribableModel.of(PasswordParameterDefinition.class).getParameter("defaultValue") != null) { 281 | return step; 282 | } 283 | Map newStepArgs = copyMapReplacingEntry(step.getArguments(), "parameters", "parameters", List.class, parameters -> parameters.stream() 284 | .map(parameter -> { 285 | if (parameter instanceof UninstantiatedDescribable) { 286 | UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter; 287 | if (ud.getSymbol().equals("password")) { 288 | Map newParamArgs = copyMapReplacingEntry(ud.getArguments(), "defaultValueAsSecret", "defaultValue", Secret.class, Secret::getPlainText); 289 | return ud.withArguments(newParamArgs); 290 | } 291 | } 292 | return parameter; 293 | }) 294 | .collect(Collectors.toList()) 295 | ); 296 | return step.withArguments(newStepArgs); 297 | } 298 | 299 | /** 300 | * Copy a map, replacing the entry with the specified key if it matches the specified type. 301 | */ 302 | private static Map copyMapReplacingEntry(Map map, String oldKey, String newKey, Class requiredValueType, Function replacer) { 303 | Map newMap = new TreeMap<>(); 304 | for (Map.Entry entry : map.entrySet()) { 305 | if (entry.getKey().equals(oldKey) && requiredValueType.isInstance(entry.getValue())) { 306 | newMap.put(newKey, replacer.apply(requiredValueType.cast(entry.getValue()))); 307 | } else { 308 | newMap.put(entry.getKey(), entry.getValue()); 309 | } 310 | } 311 | return newMap; 312 | } 313 | 314 | /** For the pipeline syntax generator page. */ 315 | public List getParametersDescriptors() { 316 | // See SECURITY-2705 on why we ban FileParemeterDefinition 317 | return ExtensionList.lookup(ParameterDescriptor.class).stream(). 318 | filter(descriptor -> descriptor.clazz != FileParameterDefinition.class). 319 | collect(Collectors.toList()); 320 | } 321 | 322 | /** 323 | * checks that the id is a valid ID. 324 | * @param id the id to check 325 | */ 326 | @Restricted(NoExternalUse.class)// jelly 327 | public FormValidation doCheckId(@QueryParameter String id) { 328 | // https://www.rfc-editor.org/rfc/rfc3986.txt 329 | // URLs may only contain ascii 330 | // and only some parts are allowed 331 | // segment = *pchar 332 | // segment-nz = 1*pchar 333 | // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) 334 | // ; non-zero-length segment without any colon ":" 335 | // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 336 | // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 337 | // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 338 | // / "*" / "+" / "," / ";" / "=" 339 | 340 | // but we are not allowing pct-encoded here. 341 | // additionally "." and ".." should be rejected. 342 | // and as we are using html / javascript in places we disallow "'" 343 | // and to prevent escaping hell disallow "&" 344 | 345 | // as well as anything unsafe we disallow . and .. (but we can have a dot inside the string so foo.bar is ok) 346 | // also Jenkins dissallows ; in the request parameter so don't allow that either. 347 | if (id == null || id.isEmpty()) { 348 | // the id will be provided by a hash of the message 349 | return FormValidation.ok(); 350 | } 351 | if (id.equals(".")) { 352 | return FormValidation.error("The ID is required to be URL safe and is limited to the characters a-z A-Z, the digits 0-9 and additionally the characters ':' '@' '=' '+' '$' ',' '-' '_' '.' '!' '~' '*' '(' ')'."); 353 | } 354 | if (id.equals("..")) { 355 | return FormValidation.error("The ID is required to be URL safe and is limited to the characters a-z A-Z, the digits 0-9 and additionally the characters ':' '@' '=' '+' '$' ',' '-' '_' '.' '!' '~' '*' '(' ')'."); 356 | } 357 | if (!id.matches("^[a-zA-Z0-9[-]._~!$()*+,:@=]+$")) { // escape the - inside another [] so it does not become a range of , - _ 358 | return FormValidation.error("The ID is required to be URL safe and is limited to the characters a-z A-Z, the digits 0-9 and additionally the characters ':' '@' '=' '+' '$' ',' '-' '_' '.' '!' '~' '*' '(' ')'."); 359 | } 360 | return FormValidation.ok(); 361 | } 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.input; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsParameterValue; 4 | import com.cloudbees.plugins.credentials.builds.CredentialsParameterBinder; 5 | import hudson.AbortException; 6 | import hudson.FilePath; 7 | import hudson.Util; 8 | import hudson.console.HyperlinkNote; 9 | import hudson.model.Describable; 10 | import hudson.model.Failure; 11 | import hudson.model.FileParameterDefinition; 12 | import hudson.model.FileParameterValue; 13 | import hudson.model.Job; 14 | import hudson.model.ModelObject; 15 | import hudson.model.ParameterDefinition; 16 | import hudson.model.ParameterValue; 17 | import hudson.model.Result; 18 | import hudson.model.Run; 19 | import hudson.model.TaskListener; 20 | import hudson.model.User; 21 | import hudson.security.ACL; 22 | import hudson.security.ACLContext; 23 | import hudson.security.SecurityRealm; 24 | import hudson.util.FormValidation; 25 | import hudson.util.FormValidation.Kind; 26 | import hudson.util.HttpResponses; 27 | import io.jenkins.servlet.ServletExceptionWrapper; 28 | import jenkins.console.ConsoleUrlProvider; 29 | import jenkins.model.IdStrategy; 30 | import jenkins.model.Jenkins; 31 | import jenkins.security.stapler.StaplerNotDispatchable; 32 | import net.sf.json.JSONArray; 33 | import net.sf.json.JSONObject; 34 | import org.acegisecurity.Authentication; 35 | import org.acegisecurity.GrantedAuthority; 36 | import org.apache.commons.lang.StringUtils; 37 | import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; 38 | import org.jenkinsci.plugins.workflow.support.actions.PauseAction; 39 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 40 | import org.kohsuke.accmod.Restricted; 41 | import org.kohsuke.accmod.restrictions.NoExternalUse; 42 | import org.kohsuke.stapler.HttpResponse; 43 | import org.kohsuke.stapler.StaplerRequest; 44 | import org.kohsuke.stapler.StaplerRequest2; 45 | import org.kohsuke.stapler.export.Exported; 46 | import org.kohsuke.stapler.export.ExportedBean; 47 | import org.kohsuke.stapler.interceptor.RequirePOST; 48 | 49 | import edu.umd.cs.findbugs.annotations.CheckForNull; 50 | import jakarta.servlet.ServletException; 51 | import java.io.IOException; 52 | import java.util.Collections; 53 | import java.util.HashMap; 54 | import java.util.HashSet; 55 | import java.util.List; 56 | import java.util.Map; 57 | import java.util.Set; 58 | import java.util.concurrent.TimeoutException; 59 | import java.util.logging.Level; 60 | import java.util.logging.Logger; 61 | 62 | import jenkins.util.SystemProperties; 63 | import jenkins.util.Timer; 64 | import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; 65 | import org.jenkinsci.plugins.workflow.steps.StepContext; 66 | 67 | /** 68 | * @author Kohsuke Kawaguchi 69 | */ 70 | @ExportedBean(defaultVisibility = 2) 71 | public class InputStepExecution extends AbstractStepExecutionImpl implements ModelObject { 72 | 73 | private static final Logger LOGGER = Logger.getLogger(InputStepExecution.class.getName()); 74 | 75 | // for testing only 76 | static final String UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME = InputStepExecution.class.getName() + ".supportUnsafeParameters"; 77 | 78 | private static boolean isAllowUnsafeParameters() { 79 | return SystemProperties.getBoolean(UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME); 80 | } 81 | 82 | /** 83 | * Result of the input. 84 | */ 85 | private Outcome outcome; 86 | 87 | final InputStep input; 88 | 89 | InputStepExecution(InputStep input, StepContext context) { 90 | super(context); 91 | this.input = input; 92 | } 93 | 94 | @Override 95 | public boolean start() throws Exception { 96 | // SECURITY-2705 if the escape hatch is allowed just warn about pending removal, otherwise fail the build before waiting 97 | if (getHasUnsafeParameters()) { 98 | if (isAllowUnsafeParameters()) { 99 | getListener().getLogger().println("Support for FileParameters in the input step has been enabled via " 100 | + UNSAFE_PARAMETER_ALLOWED_PROPERTY_NAME + " which will be removed in a future release." + 101 | System.lineSeparator() + 102 | "Details on how to migrate your pipeline can be found online: https://jenkins.io/redirect/plugin/pipeline-input-step/file-parameters."); 103 | } else { 104 | throw new AbortException("Support for FileParameters in the input step is disabled and will be removed in a future release. " + 105 | System.lineSeparator() + "Details on how to migrate your pipeline can be found online: " + 106 | "https://jenkins.io/redirect/plugin/pipeline-input-step/file-parameters."); 107 | } 108 | } 109 | if (getHasUnsafeId()) { 110 | getListener().getLogger().println("The following 'input' is using an unsafe 'id', please change the 'id' to prevent future breakage"); 111 | } 112 | 113 | Run run = getRun(); 114 | TaskListener listener = getListener(); 115 | FlowNode node = getNode(); 116 | 117 | // record this input 118 | getPauseAction().add(this); 119 | 120 | // This node causes the flow to pause at this point so we mark it as a "Pause Node". 121 | node.addAction(new PauseAction("Input")); 122 | 123 | String baseUrl = '/' + run.getUrl() + getPauseAction().getUrlName() + '/'; 124 | //JENKINS-40594 submitterParameter does not work without at least one actual parameter 125 | if (input.getParameters().isEmpty() && input.getSubmitterParameter() == null) { 126 | String thisUrl = baseUrl + Util.rawEncode(getId()) + '/'; 127 | listener.getLogger().printf("%s%n%s or %s%n", input.getMessage(), 128 | POSTHyperlinkNote.encodeTo(thisUrl + "proceedEmpty", input.getOk()), 129 | POSTHyperlinkNote.encodeTo(thisUrl + "abort", input.getCancel())); 130 | } else { 131 | // TODO listener.hyperlink(…) does not work; why? 132 | // TODO would be even cooler to embed the parameter form right in the build log (hiding it after submission) 133 | listener.getLogger().println(HyperlinkNote.encodeTo(baseUrl, "Input requested")); 134 | } 135 | return false; 136 | } 137 | 138 | @Override 139 | public void stop(Throwable cause) throws Exception { 140 | outcome = new Outcome(null,cause); 141 | // JENKINS-37154: we might be inside the VM thread, so do not do anything which might block on the VM thread 142 | Timer.get().submit(new Runnable() { 143 | @Override public void run() { 144 | try (ACLContext context = ACL.as2(ACL.SYSTEM2)) { 145 | postSettlement(); 146 | } catch (IOException | InterruptedException x) { 147 | LOGGER.log(Level.WARNING, "failed to abort " + getContext(), x); 148 | } 149 | } 150 | }); 151 | super.stop(cause); 152 | } 153 | 154 | @Exported 155 | public String getId() { 156 | return input.getId(); 157 | } 158 | 159 | @Exported 160 | public InputStep getInput() { 161 | return input; 162 | } 163 | 164 | public Run getRun() throws IOException, InterruptedException { 165 | return getContext().get(Run.class); 166 | } 167 | 168 | private FlowNode getNode() throws InterruptedException, IOException { 169 | return getContext().get(FlowNode.class); 170 | } 171 | 172 | private TaskListener getListener() throws IOException, InterruptedException { 173 | return getContext().get(TaskListener.class); 174 | } 175 | 176 | /** 177 | * If this input step has been decided one way or the other. 178 | */ 179 | @Exported 180 | public boolean isSettled() { 181 | return outcome!=null; 182 | } 183 | 184 | /** 185 | * Gets the {@link InputAction} that this step should be attached to. 186 | */ 187 | private InputAction getPauseAction() throws IOException, InterruptedException { 188 | Run run = getRun(); 189 | InputAction a = run.getAction(InputAction.class); 190 | if (a==null) 191 | run.addAction(a=new InputAction()); 192 | return a; 193 | } 194 | 195 | @Override @Exported 196 | public String getDisplayName() { 197 | String message = getInput().getMessage(); 198 | if (message.length()<32) return message; 199 | return message.substring(0,32)+"..."; 200 | } 201 | 202 | 203 | /** 204 | * Called from the form via browser to submit/abort this input step. 205 | */ 206 | @RequirePOST 207 | public HttpResponse doSubmit(StaplerRequest2 request) throws IOException, ServletException, InterruptedException { 208 | Run run = getRun(); 209 | if (request.getParameter("proceed")!=null) { 210 | doProceed(request); 211 | } else { 212 | doAbort(); 213 | } 214 | 215 | // go back to the Run console page 216 | return HttpResponses.redirectTo(ConsoleUrlProvider.getRedirectUrl(run)); 217 | } 218 | 219 | /** 220 | * REST endpoint to submit the input. 221 | */ 222 | @RequirePOST 223 | public HttpResponse doProceed(StaplerRequest2 request) throws IOException, ServletException, InterruptedException { 224 | preSubmissionCheck(); 225 | Map v = parseValue(request); 226 | return proceed(v); 227 | } 228 | 229 | /** 230 | * @deprecated use {@link #doProceed(StaplerRequest2)} 231 | */ 232 | @Deprecated 233 | @StaplerNotDispatchable 234 | public HttpResponse doProceed(StaplerRequest req) throws IOException, javax.servlet.ServletException, InterruptedException { 235 | try { 236 | return doProceed(StaplerRequest.toStaplerRequest2(req)); 237 | } catch (ServletException e) { 238 | throw ServletExceptionWrapper.fromJakartaServletException(e); 239 | } 240 | } 241 | 242 | /** 243 | * Processes the acceptance (approval) request. 244 | * This method is used by both {@link #doProceedEmpty()} and {@link #doProceed(StaplerRequest2)} 245 | * 246 | * @param params A map that represents the parameters sent in the request 247 | * @return A HttpResponse object that represents Status code (200) indicating the request succeeded normally. 248 | */ 249 | public HttpResponse proceed(@CheckForNull Map params) throws IOException, InterruptedException { 250 | User user = User.current(); 251 | String approverId = null; 252 | if (user != null){ 253 | approverId = user.getId(); 254 | getRun().addAction(new ApproverAction(approverId)); 255 | getListener().getLogger().println("Approved by " + hudson.console.ModelHyperlinkNote.encodeTo(user)); 256 | } 257 | getNode().addAction(new InputSubmittedAction(approverId, params)); 258 | 259 | Object v; 260 | if (params != null && params.size() == 1) { 261 | v = params.values().iterator().next(); 262 | } else { 263 | v = params; 264 | } 265 | outcome = new Outcome(v, null); 266 | postSettlement(); 267 | getContext().onSuccess(v); 268 | 269 | return HttpResponses.ok(); 270 | } 271 | 272 | @Deprecated 273 | @SuppressWarnings("unchecked") 274 | public HttpResponse proceed(Object v) throws IOException, InterruptedException { 275 | if (v instanceof Map) { 276 | return proceed(new HashMap((Map) v)); 277 | } else if (v == null) { 278 | return proceed(null); 279 | } else { 280 | return proceed(Collections.singletonMap("parameter", v)); 281 | } 282 | } 283 | 284 | /** 285 | * Used from the Proceed hyperlink when no parameters are defined. 286 | */ 287 | @RequirePOST 288 | public HttpResponse doProceedEmpty() throws IOException, InterruptedException { 289 | preSubmissionCheck(); 290 | 291 | return proceed(null); 292 | } 293 | 294 | /** 295 | * REST endpoint to abort the workflow. 296 | */ 297 | @RequirePOST 298 | public HttpResponse doAbort() throws IOException, InterruptedException { 299 | preAbortCheck(); 300 | 301 | FlowInterruptedException e = new FlowInterruptedException(Result.ABORTED, new Rejection(User.current())); 302 | outcome = new Outcome(null,e); 303 | postSettlement(); 304 | getContext().onFailure(e); 305 | 306 | // TODO: record this decision to FlowNode 307 | 308 | return HttpResponses.ok(); 309 | } 310 | 311 | /** 312 | * Check if the current user can abort/cancel the run from the input. 313 | */ 314 | private void preAbortCheck() throws IOException, InterruptedException { 315 | if (isSettled()) { 316 | throw new Failure("This input has been already given"); 317 | } if (!canCancel() && !canSubmit()) { 318 | if (input.getSubmitter() != null) { 319 | throw new Failure("You need to be '" + input.getSubmitter() + "' (or have Job/Cancel permissions) to cancel this."); 320 | } else { 321 | throw new Failure("You need to have Job/Cancel permissions to cancel this."); 322 | } 323 | } 324 | } 325 | 326 | /** 327 | * Check if the current user can submit the input. 328 | */ 329 | public void preSubmissionCheck() throws IOException, InterruptedException { 330 | if (isSettled()) 331 | throw new Failure("This input has been already given"); 332 | if (!canSubmit()) { 333 | if (input.getSubmitter() != null) { 334 | throw new Failure("You need to be " + input.getSubmitter() + " to submit this."); 335 | } else { 336 | throw new Failure("You need to have Job/Build permissions to submit this."); 337 | } 338 | } 339 | } 340 | 341 | private void postSettlement() throws IOException, InterruptedException { 342 | try { 343 | getPauseAction().remove(this); 344 | getRun().save(); 345 | } catch (IOException | InterruptedException | TimeoutException x) { 346 | LOGGER.log(Level.WARNING, "failed to remove InputAction from " + getContext(), x); 347 | } finally { 348 | FlowNode node = getNode(); 349 | if (node != null) { 350 | try { 351 | PauseAction.endCurrentPause(node); 352 | } catch (IOException x) { 353 | LOGGER.log(Level.WARNING, "failed to end PauseAction in " + getContext(), x); 354 | } 355 | } else { 356 | LOGGER.log(Level.WARNING, "cannot set pause end time for {0} in {1}", new Object[] {getId(), getContext()}); 357 | } 358 | } 359 | } 360 | 361 | private boolean canCancel() throws IOException, InterruptedException { 362 | return !Jenkins.get().isUseSecurity() || getRun().getParent().hasPermission(Job.CANCEL); 363 | } 364 | 365 | private boolean canSubmit() throws IOException, InterruptedException { 366 | Authentication a = Jenkins.getAuthentication(); 367 | return canSettle(a); 368 | } 369 | 370 | /** 371 | * Checks if the given user can settle this input. 372 | */ 373 | private boolean canSettle(Authentication a) throws IOException, InterruptedException { 374 | String submitter = input.getSubmitter(); 375 | if (submitter==null) 376 | return getRun().getParent().hasPermission(Job.BUILD); 377 | if (!Jenkins.get().isUseSecurity() || Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 378 | return true; 379 | } 380 | final Set submitters = new HashSet<>(); 381 | Collections.addAll(submitters, submitter.split(",")); 382 | final SecurityRealm securityRealm = Jenkins.get().getSecurityRealm(); 383 | if (isMemberOf(a.getName(), submitters, securityRealm.getUserIdStrategy())) 384 | return true; 385 | for (GrantedAuthority ga : a.getAuthorities()) { 386 | if (isMemberOf(ga.getAuthority(), submitters, securityRealm.getGroupIdStrategy())) 387 | return true; 388 | } 389 | return false; 390 | } 391 | 392 | /** 393 | * Checks if the provided userId is contained in the submitters list, using {@link SecurityRealm#getUserIdStrategy()} comparison algorithm. 394 | * Main goal is to respect here the case sensitivity settings of the current security realm 395 | * (which default behavior is case insensitivity). 396 | * 397 | * @param userId the id of the user if it is matching one of the submitters using {@link IdStrategy#equals(String, String)} 398 | * @param submitters the list of authorized submitters 399 | * @param idStrategy the idStrategy impl to use for comparison 400 | * @return true is userId was found in submitters, false if not. 401 | * 402 | * @see {@link jenkins.model.IdStrategy#CASE_INSENSITIVE}. 403 | */ 404 | private boolean isMemberOf(String userId, Set submitters, IdStrategy idStrategy) { 405 | for (String submitter : submitters) { 406 | if (idStrategy.equals(userId, StringUtils.trim(submitter))) { 407 | return true; 408 | } 409 | } 410 | return false; 411 | } 412 | 413 | /** 414 | * Parse the submitted {@link ParameterValue}s 415 | */ 416 | private Map parseValue(StaplerRequest2 request) throws ServletException, IOException, InterruptedException { 417 | Map mapResult = new HashMap(); 418 | List defs = input.getParameters(); 419 | Set vals = new HashSet<>(defs.size()); 420 | 421 | Object params = request.getSubmittedForm().get("parameter"); 422 | if (params!=null) { 423 | for (Object o : JSONArray.fromObject(params)) { 424 | JSONObject jo = (JSONObject) o; 425 | String name = jo.getString("name"); 426 | 427 | ParameterDefinition d=null; 428 | for (ParameterDefinition def : defs) { 429 | if (def.getName().equals(name)) 430 | d = def; 431 | } 432 | if (d == null) 433 | throw new IllegalArgumentException("No such parameter definition: " + name); 434 | 435 | ParameterValue v = d.createValue(request, jo); 436 | if (v == null) { 437 | continue; 438 | } 439 | vals.add(v); 440 | mapResult.put(name, convert(name, v)); 441 | } 442 | } 443 | 444 | Run run = getRun(); 445 | CredentialsParameterBinder binder = CredentialsParameterBinder.getOrCreate(run); 446 | String userId = Jenkins.getAuthentication2().getName(); 447 | for (ParameterValue val : vals) { 448 | if (val instanceof CredentialsParameterValue) { 449 | binder.bindCredentialsParameter(userId, (CredentialsParameterValue) val); 450 | } 451 | } 452 | run.replaceAction(binder); 453 | 454 | // If a destination value is specified, push the submitter to it. 455 | String valueName = input.getSubmitterParameter(); 456 | if (valueName != null && !valueName.isEmpty()) { 457 | mapResult.put(valueName, userId); 458 | } 459 | 460 | if (mapResult.isEmpty()) { 461 | return null; 462 | } else { 463 | return mapResult; 464 | } 465 | } 466 | 467 | private Object convert(String name, ParameterValue v) throws IOException, InterruptedException { 468 | if (v instanceof FileParameterValue) { // SECURITY-2705 469 | if (isAllowUnsafeParameters()) { 470 | FileParameterValue fv = (FileParameterValue) v; 471 | FilePath fp = new FilePath(getRun().getRootDir()).child(name); 472 | fp.copyFrom(fv.getFile()); 473 | return fp; 474 | } else { 475 | // whilst the step would be aborted in start() if the pipeline was in the input step at the point of 476 | // upgrade it will be allowed to pass so we pick it up here. 477 | throw new AbortException("Support for FileParameters in the input step is disabled and will be removed in a future release. " + 478 | System.lineSeparator() + "Details on how to migrate your pipeline can be found online: " + 479 | "https://jenkins.io/redirect/plugin/pipeline-input-step/file-parameters."); 480 | } 481 | } else { 482 | return v.getValue(); 483 | } 484 | } 485 | 486 | @Restricted(NoExternalUse.class) // jelly access only 487 | public boolean getHasUnsafeParameters() { 488 | return input.getParameters().stream().anyMatch(parameter -> parameter.getClass() == FileParameterDefinition.class); 489 | } 490 | 491 | @Restricted(NoExternalUse.class) // jelly access only 492 | public boolean getHasUnsafeId() { 493 | return ! input.getDescriptor().doCheckId(input.getId()).kind.equals(Kind.OK); 494 | } 495 | 496 | private static final long serialVersionUID = 1L; 497 | } 498 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputSubmittedAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017, 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 org.jenkinsci.plugins.workflow.actions.PersistentAction; 28 | 29 | import edu.umd.cs.findbugs.annotations.CheckForNull; 30 | import edu.umd.cs.findbugs.annotations.NonNull; 31 | import java.util.LinkedHashMap; 32 | import java.util.Map; 33 | 34 | public class InputSubmittedAction implements PersistentAction { 35 | 36 | /** 37 | * Parameters, if any, submitted when the input was approved. 38 | */ 39 | private final Map parameters = new LinkedHashMap<>(); 40 | 41 | /** 42 | * The user ID of the approving user. 43 | */ 44 | private final String approver; 45 | 46 | public InputSubmittedAction(String approver, @CheckForNull Map parameters) { 47 | this.approver = approver; 48 | if (parameters != null) { 49 | this.parameters.putAll(parameters); 50 | } 51 | } 52 | 53 | @NonNull 54 | public Map getParameters() { 55 | return parameters; 56 | } 57 | 58 | @CheckForNull 59 | public String getApprover() { 60 | return approver; 61 | } 62 | 63 | @Override 64 | public String getIconFileName() { 65 | return null; 66 | } 67 | 68 | @Override 69 | public String getDisplayName() { 70 | return Messages.input_submitted(); 71 | } 72 | 73 | @Override 74 | public String getUrlName() { 75 | return null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/Outcome.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.input; 2 | 3 | import java.io.Serializable; 4 | import java.lang.reflect.InvocationTargetException; 5 | 6 | /** 7 | * Result of an evaluation. 8 | * 9 | * Either represents a value in case of a normal return, or a throwable object in case of abnormal return. 10 | * Note that both fields can be null, in which case it means a normal return of the value 'null'. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | public final class Outcome implements Serializable { 15 | private final Object normal; 16 | private final Throwable abnormal; 17 | 18 | public Outcome(Object normal, Throwable abnormal) { 19 | assert normal==null || abnormal==null; 20 | this.normal = normal; 21 | this.abnormal = abnormal; 22 | } 23 | 24 | /** 25 | * Like {@link #replay()} but wraps the throwable into {@link InvocationTargetException}. 26 | */ 27 | public Object wrapReplay() throws InvocationTargetException { 28 | if (abnormal!=null) 29 | throw new InvocationTargetException(abnormal); 30 | else 31 | return normal; 32 | } 33 | 34 | public Object replay() throws Throwable { 35 | if (abnormal!=null) 36 | throw abnormal; 37 | else 38 | return normal; 39 | } 40 | 41 | public Object getNormal() { 42 | return normal; 43 | } 44 | 45 | public Throwable getAbnormal() { 46 | return abnormal; 47 | } 48 | 49 | public boolean isSuccess() { 50 | return abnormal==null; 51 | } 52 | 53 | public boolean isFailure() { 54 | return abnormal!=null; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | if (abnormal!=null) return "abnormal["+abnormal+']'; 60 | else return "normal["+normal+']'; 61 | } 62 | 63 | private static final long serialVersionUID = 1L; 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/Rejection.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.workflow.support.steps.input; 2 | 3 | import hudson.model.User; 4 | import edu.umd.cs.findbugs.annotations.CheckForNull; 5 | import jenkins.model.CauseOfInterruption; 6 | import org.kohsuke.stapler.export.Exported; 7 | 8 | /** 9 | * Indicates that the input step was rejected by the user. 10 | */ 11 | public final class Rejection extends CauseOfInterruption { 12 | 13 | private static final long serialVersionUID = 1; 14 | 15 | private final @CheckForNull String userName; 16 | private final long timestamp; 17 | 18 | public Rejection(@CheckForNull User u) { 19 | this.userName = u==null ? null : u.getId(); 20 | this.timestamp = System.currentTimeMillis(); 21 | } 22 | 23 | /** 24 | * Gets the user who rejected this. 25 | */ 26 | @Exported 27 | public @CheckForNull User getUser() { 28 | return userName != null ? User.get(userName) : null; 29 | } 30 | 31 | /** 32 | * Gets the timestamp when the rejection occurred. 33 | */ 34 | @Exported 35 | public long getTimestamp() { 36 | return timestamp; 37 | } 38 | 39 | @Override public String getShortDescription() { 40 | User u = getUser(); 41 | if (u != null) { 42 | return Messages.rejected_by(u.getDisplayName()); 43 | } else { 44 | return Messages.rejected(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Step that waits for a human being to provide and input. 3 | * 4 | * The workflow version of {@link javax.swing.JOptionPane}. 5 | */ 6 | package org.jenkinsci.plugins.workflow.support.steps.input; -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | Adds the Pipeline step input to wait for human input or approval. 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/ApproverAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | ${%approved_by_user(it.userId, it.userName, rootURL)}. 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/ApproverAction/summary.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2013-2014, CloudBees, Inc. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | approved_by_user=This was approved by user {1} -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputAction/index.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-cancel.html: -------------------------------------------------------------------------------- 1 |

2 | You can customize the text for the "abort" button to better match the context. 3 |

4 | 5 |

Usage: input message: 'Would you like to skip the next step?', ok: "Yes", cancel: "No, run anyway"

6 | 7 |

Which will look like: 8 |

 9 |         Would you like to skip the next step?
10 |         Yes or No, run anyway
11 |     
12 |

13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-id.html: -------------------------------------------------------------------------------- 1 |

2 | Every input step has an unique identifier. It is used in the generated URL to proceed or abort. 3 |

4 |

5 | A specific identifier could be used, for example, to mechanically respond to the input from some external process/tool. 6 |

7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-message.html: -------------------------------------------------------------------------------- 1 |

2 | This parameter gives a prompt which will be shown to a human: 3 |

    Ready to go?
4 |     Proceed or Abort
5 |     
6 |

7 |

8 | If you click "Proceed" the build will proceed to the next step, if you click "Abort" the build will be aborted. 9 |

-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-ok.html: -------------------------------------------------------------------------------- 1 |

2 | You can customize the text for the "proceed" button to better match the context. 3 |

4 | 5 |

Usage: input message: 'Do you approve?', ok: "Yes"

6 | 7 |

Which will look like: 8 |

 9 |         Do you approve?
10 |         Yes or Abort
11 |     
12 |

13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-parameters.html: -------------------------------------------------------------------------------- 1 |

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 |

10 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitter.html: -------------------------------------------------------------------------------- 1 |
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 |
6 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitterParameter.html: -------------------------------------------------------------------------------- 1 |
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 |
7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

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 |

11 |
12 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution/index.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 |

${it.input.message}

8 | 9 | 10 | 11 | 12 |
13 |
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 obj = actions.stream().filter(oo -> 657 | ((JSONObject)oo).get("_class").equals("org.jenkinsci.plugins.workflow.support.steps.input.InputAction") 658 | ).findFirst(); 659 | assertTrue(obj.isPresent()); 660 | JSONObject o = (JSONObject)obj.get(); 661 | assertTrue(o.has("waitingForInput")); 662 | assertTrue(o.getBoolean("waitingForInput")); 663 | 664 | InputAction inputAction = b.getAction(InputAction.class); 665 | InputStepExecution is = inputAction.getExecutions().get(0); 666 | HtmlPage p = webClient.getPage(b, inputAction.getUrlName()); 667 | j.submit(p.getFormByName(is.getId()), "proceed"); 668 | 669 | json = webClient.getJSON(b.getUrl() + "api/json?depth=1"); 670 | actions = json.getJSONObject().getJSONArray("actions"); 671 | obj = actions.stream().filter(oo -> 672 | ((JSONObject)oo).get("_class").equals("org.jenkinsci.plugins.workflow.support.steps.input.InputAction") 673 | ).findFirst(); 674 | assertTrue(obj.isPresent()); 675 | o = (JSONObject)obj.get(); 676 | assertTrue(o.has("waitingForInput")); 677 | assertFalse(o.getBoolean("waitingForInput")); 678 | } 679 | 680 | @Test 681 | public void test_api_contains_details() throws Exception { 682 | //set up dummy security real 683 | j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); 684 | // job setup 685 | WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo"); 686 | foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList( 687 | "def chosen = input message: 'Can we settle on this thing?', cancel: 'Nope', ok: 'Yep', parameters: [choice(choices: ['Apple', 'Blueberry', 'Banana'], description: 'The fruit in question.', name: 'fruit')], submitter: 'bobby', submitterParameter: 'dd'", 688 | "echo(\"after: ${chosen}\");"),"\n"),true)); 689 | 690 | // get the build going, and wait until workflow pauses 691 | QueueTaskFuture q = foo.scheduleBuild2(0); 692 | WorkflowRun b = q.getStartCondition().get(); 693 | j.waitForMessage("Input requested", b); 694 | 695 | final JenkinsRule.WebClient webClient = j.createWebClient(); 696 | final JenkinsRule.JSONWebResponse json = webClient.getJSON(b.getUrl() + "api/json?depth=2"); 697 | final JSONArray actions = json.getJSONObject().getJSONArray("actions"); 698 | final Optional obj = actions.stream().filter(oo -> 699 | ((JSONObject)oo).get("_class").equals("org.jenkinsci.plugins.workflow.support.steps.input.InputAction") 700 | ).findFirst(); 701 | assertTrue(obj.isPresent()); 702 | final JSONObject o = (JSONObject)obj.get(); 703 | assertTrue(o.has("waitingForInput")); 704 | assertTrue(o.getBoolean("waitingForInput")); 705 | 706 | assertTrue(o.has("executions")); 707 | JSONObject exs = o.getJSONArray("executions").getJSONObject(0); 708 | assertEquals("Can we settle on this thing?", exs.getString("displayName")); 709 | assertTrue(exs.has("input")); 710 | JSONObject input = exs.getJSONObject("input"); 711 | assertEquals("Can we settle on this thing?", input.getString("message")); 712 | assertEquals("Nope", input.getString("cancel")); 713 | assertEquals("Yep", input.getString("ok")); 714 | assertEquals("bobby", input.getString("submitter")); 715 | assertTrue(input.has("parameters")); 716 | JSONObject param = input.getJSONArray("parameters").getJSONObject(0); 717 | assertEquals("fruit", param.getString("name")); 718 | assertEquals("ChoiceParameterDefinition", param.getString("type")); 719 | assertThat(param.getJSONArray("choices").toArray(), arrayContaining("Apple", "Blueberry", "Banana")); 720 | 721 | InputAction inputAction = b.getAction(InputAction.class); 722 | InputStepExecution is = inputAction.getExecutions().get(0); 723 | HtmlPage p = webClient.getPage(b, inputAction.getUrlName()); 724 | j.submit(p.getFormByName(is.getId()), "proceed"); 725 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 726 | } 727 | } 728 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9edfbbe09847e1bfee4f8d2b0abfd1c3 7 | 8 | 9 | 10 | 1 11 | 1641319957316 12 | 1641319957331 13 | 0 14 | UTF-8 15 | false 16 | 17 | SUCCESS 18 | 19 | 20 | MAX_SURVIVABILITY 21 | 22 | 23 | flowNode 24 | 40682675 25 | 26 | 27 | classLoad 28 | 115296295 29 | 30 | 31 | run 32 | 178764716 33 | 34 | 35 | parse 36 | 228948941 37 | 38 | 39 | saveProgram 40 | 30298839 41 | 42 | 43 | true 44 | 3 45 | 1:3 46 | 2 47 | false 48 | false 49 | 50 | false 51 | 52 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/log: -------------------------------------------------------------------------------- 1 | Started 2 | Running in Durability level: MAX_SURVIVABILITY 3 | ha:////4AHtxTedjO4FuWXPjSlyrgu4Um+K+B3sX8cvS+a2ZmFQAAAAoh+LCAAAAAAAAP9tjTEOwjAQBM8BClpKHuFItIiK1krDC0x8GCfWnbEdkooX8TX+gCESFVvtrLSa5wtWKcKBo5UdUu8otU4GP9jS5Mixv3geZcdn2TIl9igbHBs2eJyx4YwwR1SwULBGaj0nRzbDRnX6rmuvydanHMu2V1A5c4MHCFXMWcf8hSnC9jqYxPTz/BXAFEIGsfuclm8zQVqFvQAAAA==[Pipeline] Start of Pipeline 4 | ha:////4DWAM7VJIWqcnvDJRWFKfsxFCA/FkH4QhONE5L3fenAjAAAAoh+LCAAAAAAAAP9tjTEOAiEURD9rLGwtPQSbaGmsbAmNJ0AWEZb8zwLrbuWJvJp3kLiJlZNMMm+a93rDOic4UbLcG+wdZu14DKOti0+U+lugiXu6ck2YKRguzSSpM+cFJRUDS1gDKwEbgzpQdmgLbIVXD9UGhba9lFS/o4DGdQM8gYlqLiqVL8wJdvexy4Q/z18BzLEA29ce4gfya1RxvAAAAA==[Pipeline] input 5 | please approve 6 | ha:////4ASquRpshQDZtU63AGmx1s9tkRU5NJPpkDRM+cfprGmZAAABFx+LCAAAAAAAAP9djztOA0EQRHvXAlJEiMjIZ7xg/EOOAAkkC5BMRLaf3u8wPcz0YjvxIbgGJEhcgUNwATJiAhIWlgQqqUpeSe/xHdachWOymShRV4V2cSGMqrNmiTnZKlU0F642hiwLx2icKLSpWVxezK5OlwatKnR1Toybr53nj8Hnmw/eFDq1VQxb0zK8C6UKdSZnbAudHS4s7OR14kiLmLQjheLPS3/y8DS5Ny8++GewrlBnnLd3t7ACr8G3/+FHbX/D0MbzARamGRsM3i7Ddc5sxlIqikOVk+Nxrx8cBPLXWJYUSSMD+SMmR5ikUYTd0bA3wCBKEXvpMNmLumGUJkG8L42lGDE5uTG8/AIfag+3QQEAAA==Proceed or ha:////4BXEck/5OQrXT2Bv0zBnv+p+jVwK6DnLZsZwcVW57/iTAAABER+LCAAAAAAAAP9dkD1OxDAUhF+yWmpEiejo7Q2E/UNbQQHSakEKF7CTl1/LNvYLuzQcgmtAg8QVOAQXoKOmoCEQGphmpvlGmnl8h6F3cGpcwWrUTaV9WjGr2qJLbG1ckyuzZr611jhintB6VmnbEru8SK7Obi06VelmZQi3XwfPH5PPtxCCJQxapwh2lrW4EVwJXfCEXKWL442DvbLNvNEsNdobhexPy3jx8LS4ty8hhOewpVAXVPZ113AHQYfv/sNPev+GoVcQAmxsF4YEwT5BUhLZOefKpEKVxtM8HkdHEf9dzGsjueUR/xnGZ5jlUuJoNo0nGMkcMc6n2YEcCZlnUXrIhey++AKVT7SLOgEAAA==Abort 7 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/log-index: -------------------------------------------------------------------------------- 1 | 667 3 2 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/program.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-input-step-plugin/d61b1d3c5078c3948684cbacaa3b503c8c7ae881/src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/program.dat -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/workflow/2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2 6 | 7 | 8 | 9 | 1641319957693 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/1/workflow/3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2 6 | 7 | 3 8 | org.jenkinsci.plugins.workflow.support.steps.input.InputStep 9 | 10 | 11 | 12 | 13 | 14 | message 15 | please approve 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 1641319957799 23 | 24 | 25 | 26 | Input 27 | 1641319957853 28 | 0 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/legacyIds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-input-step-plugin/d61b1d3c5078c3948684cbacaa3b503c8c7ae881/src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/legacyIds -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/builds/permalinks: -------------------------------------------------------------------------------- 1 | lastSuccessfulBuild -1 2 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | 7 | true 8 | 9 | 10 | false 11 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/jobs/p/nextBuildNumber: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest/serialForm/org.jenkinsci.plugins.workflow.flow.FlowExecutionList.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | p 5 | 1 6 | 7 | --------------------------------------------------------------------------------