Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-62305] Make password parameters work with the build step in Jenkins 2.236+ #46

Merged
merged 12 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
buildPlugin()
buildPlugin(useAci: true, configurations: [
[ platform: 'linux', jdk: '8' ],
[ platform: 'windows', jdk: '8' ],
[ platform: 'linux', jdk: '11', jenkins: '2.236' ]
])
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@

Adds the Pipeline `build` step, which triggers builds of other jobs.

### Passing secret values to other jobs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention also in help-parameters.html.


The recommended approach to pass secret values using the `build` step is to use credentials parameters:

```
build(job: 'foo', parameters: [credentials('parameter-name', 'credentials-id')])
```

See [the user guide for the Credentials Plugin](https://plugins.jenkins.io/credentials/) for a general overview of how credentials work in Jenkins and how they can be configured, and [the documentation for the Credentials Binding Plugin](https://plugins.jenkins.io/credentials-binding/) for an overview of how to access and use credentials from a Pipeline.

The `build` step also supports passing password parameters, but this is not recommended.
The plaintext secret may be persisted as part of the Pipeline's internal state, and it will not be automatically masked if it appears in the build log.
Here is an example for reference:

```
build(job: 'foo', parameters: [password('parameter-name', 'secret-value')])
```

## Version History

See [the changelog](CHANGELOG.md).
42 changes: 16 additions & 26 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>3.54</version>
<version>4.4</version>
<relativePath />
</parent>
<groupId>org.jenkins-ci.plugins</groupId>
Expand Down Expand Up @@ -40,108 +40,99 @@
<properties>
<revision>2.13</revision>
<changelist>-SNAPSHOT</changelist>
<jenkins.version>2.138.4</jenkins.version>
<jenkins.version>2.176.4</jenkins.version>
<java.level>8</java.level>
<scm-api.version>2.2.7</scm-api.version>
<workflow-step-api-plugin.version>2.22</workflow-step-api-plugin.version>
<workflow-support-plugin.version>3.1</workflow-support-plugin.version>
<workflow-cps-plugin.version>2.62</workflow-cps-plugin.version>
<useBeta>true</useBeta>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.176.x</artifactId>
<version>11</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>${workflow-step-api-plugin.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.33</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>${workflow-support-plugin.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.50</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
<version>1.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>${workflow-step-api-plugin.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>${workflow-support-plugin.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${workflow-cps-plugin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${workflow-cps-plugin.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<version>2.31</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<version>2.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>branch-api</artifactId>
<version>2.1.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>2.1.18</version>
<scope>test</scope>
</dependency>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>${scm-api.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-scm-step</artifactId>
<version>2.7</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -153,7 +144,6 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-folder</artifactId>
<version>6.4</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PasswordParameterValue;
import hudson.model.Queue;
import hudson.util.FormValidation;
import hudson.util.Secret;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.structs.describable.CustomDescribableModel;
import org.jenkinsci.plugins.structs.describable.DescribableModel;
import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.Step;
Expand Down Expand Up @@ -80,15 +89,15 @@ public boolean isPropagate() {
}

@Extension
public static class DescriptorImpl extends AbstractStepDescriptorImpl {
public static class DescriptorImpl extends AbstractStepDescriptorImpl implements CustomDescribableModel {

public DescriptorImpl() {
super(BuildTriggerStepExecution.class);
}

// Note: This is necessary because the JSON format of the parameters produced by config.jelly when
// using the snippet generator does not match what would be neccessary for databinding to work automatically.
// For non-snippet generator use, this is unnecessary.
// Only called via the snippet generator.
@Override public Step newInstance(StaplerRequest req, JSONObject formData) throws FormException {
BuildTriggerStep step = (BuildTriggerStep) super.newInstance(req, formData);
// Cf. ParametersDefinitionProperty._doBuild:
Expand Down Expand Up @@ -122,6 +131,64 @@ public DescriptorImpl() {
return step;
}

/**
* Compatibility hack for JENKINS-62305. Only affects runtime behavior of the step, not the snippet generator.
* Ideally, password parameters would not be used at all with this step, but there was no documentation or
* runtime warnings for this usage previously and so it is relatively common.
*/
@Override
public Map<String, Object> customInstantiate(Map<String, Object> map) {
if (DescribableModel.of(PasswordParameterValue.class).getParameter("value").getErasedType() != Secret.class) {
return map;
}
return copyMapReplacingEntry(map, "parameters", List.class, parameters -> parameters.stream()
.map(parameter -> {
if (parameter instanceof UninstantiatedDescribable) {
UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter;
if (ud.getSymbol().equals("password")) {
Map<String, Object> newArguments = copyMapReplacingEntry(ud.getArguments(), "value", String.class, Secret::fromString);
return ud.withArguments(newArguments);
}
}
return parameter;
})
.collect(Collectors.toList())
);
}

@Override
public UninstantiatedDescribable customUninstantiate(UninstantiatedDescribable step) {
Map<String, Object> newStepArgs = copyMapReplacingEntry(step.getArguments(), "parameters", List.class, parameters -> parameters.stream()
.map(parameter -> {
if (parameter instanceof UninstantiatedDescribable) {
UninstantiatedDescribable ud = (UninstantiatedDescribable) parameter;
if (ud.getSymbol().equals("password")) {
Map<String, Object> newParamArgs = copyMapReplacingEntry(ud.getArguments(), "value", Secret.class, Secret::getPlainText);
return ud.withArguments(newParamArgs);
}
}
return parameter;
})
.collect(Collectors.toList())
);
return step.withArguments(newStepArgs);
}

/**
* Copy a map, replacing the entry with the specified key if it matches the specified type.
*/
private static <T> Map<String, Object> copyMapReplacingEntry(Map<String, ?> map, String keyToReplace, Class<T> requiredValueType, Function<T, Object> replacer) {
Map<String, Object> newMap = new HashMap<>();
for (Map.Entry<String, ?> entry : map.entrySet()) {
if (entry.getKey().equals(keyToReplace) && requiredValueType.isInstance(entry.getValue())) {
newMap.put(entry.getKey(), replacer.apply(requiredValueType.cast(entry.getValue())));
} else {
newMap.put(entry.getKey(), entry.getValue());
}
}
return newMap;
}

@Override
public String getFunctionName() {
return "build";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ THE SOFTWARE.
<f:entry field="quietPeriod" title="Quiet period">
<f:number clazz="number"/>
</f:entry>
<f:entry title="Parameters">
<f:entry field="parameters" title="Parameters">
<div id="params"/>
<script>
function loadParams() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>
A list of parameters to pass to the downstream job.
When passing secrets to downstream jobs, prefer credentials parameters over password parameters.
See <a href="https://plugins.jenkins.io/pipeline-build-step/">the documentation</a> for details.
jglick marked this conversation as resolved.
Show resolved Hide resolved
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jenkinsci.plugins.workflow.support.steps.build;

import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsParameterDefinition;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Launcher;
Expand All @@ -20,6 +22,7 @@
import hudson.model.ParametersAction;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PasswordParameterDefinition;
import hudson.model.PasswordParameterValue;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Run;
Expand Down Expand Up @@ -74,7 +77,10 @@
import org.jvnet.hudson.test.TestBuilder;
import org.jvnet.hudson.test.TestExtension;
import org.jvnet.hudson.test.recipes.LocalData;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assume.assumeThat;

public class BuildTriggerStepTest {

Expand Down Expand Up @@ -572,6 +578,9 @@ public void invalidChoiceParameterValue() throws Exception {
step.setParameters(Arrays.asList(new StringParameterValue("branch", "default"), new BooleanParameterValue("correct", true)));
// Note: This does not actually test the format of the JSON produced by the snippet generator for parameters, see generateSnippet* for tests of that behavior.
st.assertRoundTrip(step, "build job: 'downstream', parameters: [string(name: 'branch', value: 'default'), booleanParam(name: 'correct', value: true)]");
// Passwords parameters are handled specially via CustomDescribableModel
step.setParameters(Arrays.asList(new PasswordParameterValue("param-name", "secret")));
st.assertRoundTrip(step, "build job: 'downstream', parameters: [password(name: 'param-name', value: 'secret')]");
}

@Issue("JENKINS-26093")
Expand Down Expand Up @@ -605,7 +614,13 @@ public void invalidChoiceParameterValue() throws Exception {

@Test
public void buildStepDocs() throws Exception {
SnippetizerTester.assertDocGeneration(BuildTriggerStep.class);
try {
SnippetizerTester.assertDocGeneration(BuildTriggerStep.class);
} catch (Exception e) {
// TODO: Jenkins 2.236+ broke structs-based databinding and introspection of PasswordParameterValue, JENKINS-62305.
jglick marked this conversation as resolved.
Show resolved Hide resolved
assumeThat(e.getMessage(), not(containsString("There's no @DataBoundConstructor on any constructor of class hudson.util.Secret")));
dwnusbaum marked this conversation as resolved.
Show resolved Hide resolved
throw e;
}
}

@Test public void automaticParameterConversion() throws Exception {
Expand Down Expand Up @@ -704,6 +719,33 @@ public void buildStepDocs() throws Exception {
assertThat(parameterNames, equalTo(Arrays.asList("PARAM1", "PARAM2", "PARAM3", "PARAM4")));
}

@Issue("JENKINS-62305")
@Test public void passwordParameter() throws Exception {
WorkflowJob ds = j.createProject(WorkflowJob.class);
ds.addProperty(new ParametersDefinitionProperty(
new PasswordParameterDefinition("my-password", "", "")));
ds.setDefinition(new CpsFlowDefinition(
"echo('Password: ' + params['my-password'])\n", true));
WorkflowJob us = j.createProject(WorkflowJob.class);
us.setDefinition(new CpsFlowDefinition(
"build(job: '" + ds.getName() + "', parameters: [password(name: 'my-password', value: 'secret')])", true));
j.buildAndAssertSuccess(us);
j.assertLogContains("Password: secret", ds.getBuildByNumber(1));
}

@Test public void credentialsParameter() throws Exception {
WorkflowJob ds = j.createProject(WorkflowJob.class);
ds.addProperty(new ParametersDefinitionProperty(
new CredentialsParameterDefinition("my-credential", "", "", Credentials.class.getName(), false)));
ds.setDefinition(new CpsFlowDefinition(
"echo('Credential: ' + params['my-credential'])\n", true));
WorkflowJob us = j.createProject(WorkflowJob.class);
us.setDefinition(new CpsFlowDefinition(
"build(job: '" + ds.getName() + "', parameters: [credentials(name: 'my-credential', value: 'credential-id')])", true));
j.buildAndAssertSuccess(us);
j.assertLogContains("Credential: credential-id", ds.getBuildByNumber(1));
}

private static ParameterValue getParameter(Run<?, ?> run, String parameterName) {
return run.getAction(ParametersAction.class).getParameter(parameterName);
}
Expand Down