Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
009bad3
added unit test showing leaked password
Jul 14, 2020
52f6f4b
working PoC
Jul 24, 2020
75c7406
add listener to report errors to pipeline output
Jul 24, 2020
191f08e
PoC 2 for groovy interpolation interception. Does not require core mods
Jul 28, 2020
ccf9040
Wrap EnvironmentExpander and EnvVars together for parseArgs
Jul 31, 2020
5c53b94
use incrementals, revert jenkins version
Jul 31, 2020
c5ef8ad
code cleanup
Jul 31, 2020
1a7793d
catch null arguments
Jul 31, 2020
0829025
Make unit test windows friendly. Remove dollar sign from password
Jul 31, 2020
18653c0
update to use newer implementation of EnvironmentExpander
Aug 4, 2020
5ba9b2a
Use updated api in EnvironmentExpander
Aug 5, 2020
aeebf17
address review comments
Aug 27, 2020
02804b3
add factory method, add report action, and summary page
Sep 1, 2020
20d781d
change from table to list, change icon
Sep 2, 2020
b404458
update jelly formatting, update unit test
Sep 5, 2020
22c4ae3
Check for empty body
Sep 8, 2020
41984a6
Update body check, support legacy stage behavior
Sep 10, 2020
d4759b1
add check for empty args
Sep 10, 2020
4a98072
fix variable clashing
Sep 10, 2020
8bdc019
address review comments
Sep 10, 2020
e975453
Refactor Action name, generate action only when there are secrets exp…
Sep 10, 2020
d4765a4
update null environment variable test
Sep 10, 2020
341972a
check for "bat" args on windows
Sep 11, 2020
ade619f
avoid reflective API
Sep 11, 2020
79d255a
address review comments
Sep 11, 2020
0abb665
address review comments
Sep 15, 2020
7a5d0c7
update jelly file path, fix localization error with parenthesis
Sep 15, 2020
506d5b3
add placeholder explanation page for jelly
Sep 15, 2020
93b018b
more refactoring of envwatcher
Sep 15, 2020
065b129
update step-api dependency
Sep 15, 2020
4903d4e
support detecting interpolation in describables
Sep 16, 2020
cc5b3b0
Track groovy strings instead of using InterpolatedSecretsDetector.
Sep 18, 2020
8517f12
added InterpolatedUninstantiatedDescribable
Sep 21, 2020
f12f226
add null checks for environmentexpander and envars
Sep 21, 2020
9a31bce
Merge remote-tracking branch 'upstream/master' into interpolation-v2
Sep 22, 2020
6e8b5eb
Refactor ArgumentsActionImpl using EnvironmentExpander and removing s…
Sep 23, 2020
eaa4c90
set sensitiveVariables as field instead of recursively passing through
Sep 23, 2020
70c4c22
Move interpolatedStrings into parseArgs
Sep 23, 2020
8a799dc
Remove duplicate code
Sep 23, 2020
1afab59
no metaStep returns NamedArgsAndClosure
Sep 25, 2020
dbe7d8f
Update error logging
Sep 25, 2020
902ab29
add windows support for unit test
Sep 25, 2020
6a39c5c
report step name and arguments that log a warning
Sep 28, 2020
ec29d4b
Handle multiple sensitive variables in one argument
Sep 28, 2020
97b1535
fix windows tests
Sep 28, 2020
b34e5fd
Merge remote-tracking branch 'upstream/master' into interpolation-v2
Sep 29, 2020
b0d183b
update documentation
Sep 29, 2020
b846c44
refactoring
Oct 1, 2020
4f9997d
fix jelly output
Oct 1, 2020
8506214
fix unit tests, some clean up
Oct 1, 2020
e6284e9
add redirect to console warning
Oct 1, 2020
ef157e9
address review comments
Oct 8, 2020
00d220b
simplify parseArgs
Oct 8, 2020
a8df396
centralize parsing of NamedArgsAndClosure
Oct 12, 2020
7c8022b
Sort arguments in step signature
Oct 12, 2020
a890c43
Merge remote-tracking branch 'upstream/master' into interpolation-v2
Oct 12, 2020
36050b4
fix comments
Oct 13, 2020
163e7cb
make step arguments print out in order they were added
Oct 14, 2020
3740ed1
update workflow-step-api and credentials-binding to release versions
Oct 15, 2020
a20db3d
update bom
Oct 19, 2020
b14f8cf
bump bom to v15
Oct 19, 2020
d489f73
address review comments
Oct 19, 2020
11b12ba
make getStepSignature recursive
Oct 20, 2020
bd0dfd2
make recursion more generic, add unit test for getStepSignature()
Oct 20, 2020
86cf9f3
parse UninstantiatedDescribable in getStepSignature
Oct 20, 2020
a24ffe0
control warning behavior with system property
Oct 20, 2020
cd43425
update unit tests with new UninstantiatedDescribable output
Oct 20, 2020
ac2ec02
Merge remote-tracking branch 'upstream/master' into interpolation-v2
Oct 27, 2020
41e50e6
address review comments
Nov 2, 2020
3e54d36
make InterpolatedWarnings.run transient field
Nov 2, 2020
b52438d
update UninstantiatedDescribable $class toString
Nov 3, 2020
49695e0
update InterpolatedSecretesAction onLoad and onAttached
Nov 3, 2020
7d8672c
Update getStepSignature to better reflect pipeline input
Nov 5, 2020
611326c
Remove printing of step signature
Nov 5, 2020
96c2f30
remove setting the model for the Uninstantiated Describable
Nov 5, 2020
7811148
Make sure password parameters are masked in step arguments
dwnusbaum Nov 5, 2020
f7798af
Remove InterpolatedSecretsActionTest.java
dwnusbaum Nov 5, 2020
40eb64a
Align workflow-support tests jar with incremental version
dwnusbaum Nov 5, 2020
bc8d268
Update to latest workflow-support incremental
dwnusbaum Nov 5, 2020
e173261
Merge remote-tracking branch 'upstream/master' into interpolation-v2
Nov 5, 2020
4524d26
update pom, update changelog to prepare for release
Nov 6, 2020
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
5 changes: 4 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.176.x</artifactId>
<version>9</version>
<version>11</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand All @@ -84,6 +84,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.23-rc566.c1fa948b49be</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand Down Expand Up @@ -124,6 +125,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.23-rc566.c1fa948b49be</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
Expand Down Expand Up @@ -157,6 +159,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials-binding</artifactId>
<version>1.24-rc370.8c9bfdc92872</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ private static final class DynamicContextQuery {
}
}

@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throws IOException;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public class CpsStepContext extends DefaultStepContext { // TODO add XStream cla
private transient List<CpsBodyInvoker> bodyInvokers = new ArrayList<>();

/**
* While {@link CpsStepContext} has not received teh response, maintains the body closure.
* While {@link CpsStepContext} has not received the response, maintains the body closure.
*
* This is the implicit closure block passed to the step invocation.
*/
Expand All @@ -181,15 +181,22 @@ public class CpsStepContext extends DefaultStepContext { // TODO add XStream cla
private transient volatile boolean loadingThreadGroup;

@CpsVmThreadOnly
CpsStepContext(StepDescriptor step, CpsThread thread, FlowExecutionOwner executionRef, FlowNode node, @CheckForNull Closure body) {
CpsStepContext(StepDescriptor step, CpsThread thread, FlowExecutionOwner executionRef, FlowNode node) {
this.threadId = thread.id;
this.executionRef = executionRef;
this.id = node.getId();
this.node = node;
this.body = body != null ? thread.group.export(body) : null;
this.stepDescriptorId = step.getId();
}

public void setBody(@Nonnull Closure bodyToSet, @Nonnull CpsThread thread) {
if (this.body == null) {
this.body = thread.group.export(bodyToSet);
} else {
throw new IllegalStateException("Context already has a body");
}
}

/**
* Obtains {@link StepDescriptor} that represents the step this context is invoking.
*
Expand Down
103 changes: 56 additions & 47 deletions src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.NoStaplerConstructorException;

import javax.annotation.Nullable;

/**
* Calls {@link Step}s and other DSL objects.
*/
Expand Down Expand Up @@ -217,23 +219,23 @@ protected Object invokeStep(StepDescriptor d, Object args) {
* @param args The arguments passed to the step.
*/
protected Object invokeStep(StepDescriptor d, String name, Object args) {
final NamedArgsAndClosure ps = parseArgs(args, d);

CpsThread thread = CpsThread.current();

FlowNode an;

// TODO: generalize the notion of Step taking over the FlowNode creation.
boolean hack = d instanceof ParallelStep.DescriptorImpl || d instanceof LoadStep.DescriptorImpl;

if (ps.body == null && !hack) {
FlowNode an;
CpsThread thread = CpsThread.current();
if (!d.takesImplicitBlockArgument() && !hack) {
an = new StepAtomNode(exec, d, thread.head.get());
} else {
an = new StepStartNode(exec, d, thread.head.get());
}

CpsStepContext context = new CpsStepContext(d, thread, handle, an);
EnvironmentWatcher envWatcher = EnvironmentWatcher.of(context, exec);
NamedArgsAndClosure ps = parseArgs(args, d, envWatcher);
context.setBody(ps.body, thread);
// Ensure ArgumentsAction is attached before we notify even synchronous listeners:
final CpsStepContext context = new CpsStepContext(d, thread, handle, an, ps.body);
try {
// No point storing empty arguments, and ParallelStep is a special case where we can't store its closure arguments
if (ps.namedArgs != null && !(ps.namedArgs.isEmpty()) && isKeepStepArguments() && !(d instanceof ParallelStep.DescriptorImpl)) {
Expand All @@ -257,16 +259,20 @@ protected Object invokeStep(StepDescriptor d, String name, Object args) {
boolean sync;
ClassLoader originalLoader = Thread.currentThread().getContextClassLoader();
try {
TaskListener listener = context.get(TaskListener.class);
if (unreportedAmbiguousFunctions.remove(name)) {
reportAmbiguousStepInvocation(context, d);
reportAmbiguousStepInvocation(context, d, listener);
}
d.checkContextAvailability(context);
Thread.currentThread().setContextClassLoader(CpsVmExecutorService.ORIGINAL_CONTEXT_CLASS_LOADER.get());
if (Util.isOverridden(StepDescriptor.class, d.getClass(), "newInstance", Map.class)) {
s = d.newInstance(ps.namedArgs);
} else {
DescribableModel<? extends Step> stepModel = DescribableModel.of(d.clazz);
s = stepModel.instantiate(ps.namedArgs, context.get(TaskListener.class));
s = stepModel.instantiate(ps.namedArgs, listener);
}
if (envWatcher != null) {
envWatcher.logResults(listener);
}

// Persist the node - block start and end nodes do their own persistence.
Expand Down Expand Up @@ -354,7 +360,7 @@ protected Object invokeDescribable(String symbol, Object _args) {

// The only time a closure is valid is when the resulting Describable is immediately executed via a meta-step
NamedArgsAndClosure args = parseArgs(_args, metaStep!=null && metaStep.takesImplicitBlockArgument(),
UninstantiatedDescribable.ANONYMOUS_KEY, singleArgumentOnly);
UninstantiatedDescribable.ANONYMOUS_KEY, singleArgumentOnly, null);
UninstantiatedDescribable ud = new UninstantiatedDescribable(symbol, null, args.namedArgs);

if (metaStep==null) {
Expand Down Expand Up @@ -414,32 +420,27 @@ protected Object invokeDescribable(String symbol, Object _args) {
ud = new UninstantiatedDescribable(symbol, null, dargs);
margs.put(p.getName(),ud);

return invokeStep(metaStep, symbol, new NamedArgsAndClosure(margs, args.body));
return invokeStep(metaStep, symbol, new NamedArgsAndClosure(margs, args.body, null));
} catch (Exception e) {
throw new IllegalArgumentException("Failed to prepare "+symbol+" step",e);
}
}
}

private void reportAmbiguousStepInvocation(CpsStepContext context, StepDescriptor d) {
private void reportAmbiguousStepInvocation(CpsStepContext context, StepDescriptor d, @Nullable TaskListener listener) {
Exception e = null;
try {
TaskListener listener = context.get(TaskListener.class);
if (listener != null) {
List<String> ambiguousClassNames = StepDescriptor.all().stream()
.filter(sd -> sd.getFunctionName().equals(d.getFunctionName()))
.map(sd -> sd.clazz.getName())
.collect(Collectors.toList());
String message = String.format("Warning: Invoking ambiguous Pipeline Step ‘%1$s’ (%2$s). " +
"‘%1$s’ could refer to any of the following steps: %3$s. " +
"You can invoke steps by class name instead to avoid ambiguity. " +
"For example: steps.'%2$s'(...)",
d.getFunctionName(), d.clazz.getName(), ambiguousClassNames);
listener.getLogger().println(message);
return;
}
} catch (InterruptedException | IOException temp) {
e = temp;
if (listener != null) {
List<String> ambiguousClassNames = StepDescriptor.all().stream()
.filter(sd -> sd.getFunctionName().equals(d.getFunctionName()))
.map(sd -> sd.clazz.getName())
.collect(Collectors.toList());
String message = String.format("Warning: Invoking ambiguous Pipeline Step ‘%1$s’ (%2$s). " +
"‘%1$s’ could refer to any of the following steps: %3$s. " +
"You can invoke steps by class name instead to avoid ambiguity. " +
"For example: steps.'%2$s'(...)",
d.getFunctionName(), d.clazz.getName(), ambiguousClassNames);
listener.getLogger().println(message);
return;
}
LOGGER.log(Level.FINE, "Unable to report ambiguous step invocation for: " + d.getFunctionName(), e);
}
Expand All @@ -458,14 +459,16 @@ private static int preallocatedHashmapCapacity(int elementsToHold) {
static class NamedArgsAndClosure {
final Map<String,Object> namedArgs;
final Closure body;
final List<String> msgs;

private NamedArgsAndClosure(Map<?,?> namedArgs, Closure body) {
private NamedArgsAndClosure(Map<?,?> namedArgs, Closure body, @Nullable EnvironmentWatcher envWatcher) {
this.namedArgs = new LinkedHashMap<>(preallocatedHashmapCapacity(namedArgs.size()));
this.body = body;
this.msgs = new ArrayList<>();

for (Map.Entry<?,?> entry : namedArgs.entrySet()) {
String k = entry.getKey().toString().intern(); // coerces GString and more
Object v = flattenGString(entry.getValue());
Object v = flattenGString(entry.getValue(), envWatcher);
this.namedArgs.put(k, v);
}
}
Expand All @@ -481,14 +484,18 @@ private NamedArgsAndClosure(Map<?,?> namedArgs, Closure body) {
* but better to do it here in the Groovy-specific code so we do not need to rely on that.
* @return {@code v} or an equivalent with all {@link GString}s flattened, including in nested {@link List}s or {@link Map}s
*/
private static Object flattenGString(Object v) {
private static Object flattenGString(Object v, @Nullable EnvironmentWatcher envWatcher) {
if (v instanceof GString) {
return v.toString();
String flattened = v.toString();
if (envWatcher != null) {
envWatcher.scan(flattened);
}
return flattened;
} else if (v instanceof List) {
boolean mutated = false;
List<Object> r = new ArrayList<>();
for (Object o : ((List<?>) v)) {
Object o2 = flattenGString(o);
Object o2 = flattenGString(o, envWatcher);
mutated |= o != o2;
r.add(o2);
}
Expand All @@ -498,9 +505,9 @@ private static Object flattenGString(Object v) {
Map<Object,Object> r = new LinkedHashMap<>(preallocatedHashmapCapacity(((Map) v).size()));
for (Map.Entry<?,?> e : ((Map<?, ?>) v).entrySet()) {
Object k = e.getKey();
Object k2 = flattenGString(k);
Object k2 = flattenGString(k, envWatcher);
Object o = e.getValue();
Object o2 = flattenGString(o);
Object o2 = flattenGString(o, envWatcher);
mutated |= k != k2 || o != o2;
r.put(k2, o2);
}
Expand All @@ -510,20 +517,20 @@ private static Object flattenGString(Object v) {
}
}

static NamedArgsAndClosure parseArgs(Object arg, StepDescriptor d) {
static NamedArgsAndClosure parseArgs(Object arg, StepDescriptor d, EnvironmentWatcher envWatcher) {
boolean singleArgumentOnly = false;
try {
DescribableModel<?> stepModel = DescribableModel.of(d.clazz);
singleArgumentOnly = stepModel.hasSingleRequiredParameter() && stepModel.getParameters().size() == 1;
if (singleArgumentOnly) { // Can fetch the one argument we need
DescribableParameter dp = stepModel.getSoleRequiredParameter();
String paramName = (dp != null) ? dp.getName() : null;
return parseArgs(arg, d.takesImplicitBlockArgument(), paramName, singleArgumentOnly);
return parseArgs(arg, d.takesImplicitBlockArgument(), paramName, singleArgumentOnly, envWatcher);
}
} catch (NoStaplerConstructorException e) {
// Ignore steps without databound constructors and treat them as normal.
}
return parseArgs(arg,d.takesImplicitBlockArgument(), loadSoleArgumentKey(d), singleArgumentOnly);
return parseArgs(arg,d.takesImplicitBlockArgument(), loadSoleArgumentKey(d), singleArgumentOnly, envWatcher);
}

/**
Expand All @@ -548,19 +555,21 @@ static NamedArgsAndClosure parseArgs(Object arg, StepDescriptor d) {
* @param soleArgumentKey
* If the context in which this method call happens allow implicit sole default argument, specify its name.
* If null, the call must be with names arguments.
// * @param envVars
* The environment variables of the context
*/
static NamedArgsAndClosure parseArgs(Object arg, boolean expectsBlock, String soleArgumentKey, boolean singleRequiredArg) {
static NamedArgsAndClosure parseArgs(Object arg, boolean expectsBlock, String soleArgumentKey, boolean singleRequiredArg, @Nullable EnvironmentWatcher envWatcher) {
if (arg instanceof NamedArgsAndClosure)
return (NamedArgsAndClosure) arg;
if (arg instanceof Map) // TODO is this clause actually used?
return new NamedArgsAndClosure((Map) arg, null);
return new NamedArgsAndClosure((Map) arg, null, envWatcher);
if (arg instanceof Closure && expectsBlock)
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),(Closure)arg);
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),(Closure)arg, envWatcher);

if (arg instanceof Object[]) {// this is how Groovy appears to pack argument list into one Object for invokeMethod
List a = Arrays.asList((Object[])arg);
if (a.size()==0)
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),null);
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),null, envWatcher);

Closure c=null;

Expand All @@ -575,21 +584,21 @@ static NamedArgsAndClosure parseArgs(Object arg, boolean expectsBlock, String so
if (!singleRequiredArg ||
(soleArgumentKey != null && mapArg.size() == 1 && mapArg.containsKey(soleArgumentKey))) {
// this is how Groovy passes in Map
return new NamedArgsAndClosure(mapArg, c);
return new NamedArgsAndClosure(mapArg, c, envWatcher);
}
}

switch (a.size()) {
case 0:
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),c);
return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),c, envWatcher);
case 1:
return new NamedArgsAndClosure(singleParam(soleArgumentKey, a.get(0)), c);
return new NamedArgsAndClosure(singleParam(soleArgumentKey, a.get(0)), c, envWatcher);
default:
throw new IllegalArgumentException("Expected named arguments but got "+a);
}
}

return new NamedArgsAndClosure(singleParam(soleArgumentKey, arg), null);
return new NamedArgsAndClosure(singleParam(soleArgumentKey, arg), null, envWatcher);
}
private static Map<String,Object> singleParam(String soleArgumentKey, Object arg) {
if (soleArgumentKey != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.jenkinsci.plugins.workflow.cps;

import hudson.EnvVars;
import hudson.model.Run;
import hudson.model.TaskListener;
import org.jenkinsci.plugins.workflow.cps.view.EnvironmentWatcherRunReport;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class EnvironmentWatcher implements Serializable {
private EnvVars envVars;
private Set<String> watchedVars;
private List<String> scanResults;
private static EnvironmentWatcherRunReport runReport;

private static final Logger LOGGER = Logger.getLogger(EnvironmentWatcher.class.getName());

public static EnvironmentWatcher of(CpsStepContext context, CpsFlowExecution exec) {
try {
EnvVars contextEnvVars = context.get(EnvVars.class);
EnvironmentExpander contextExpander = context.get(EnvironmentExpander.class);
if (contextEnvVars != null && contextExpander != null) {
if (runReport == null) {
FlowExecutionOwner owner = exec.getOwner();
if (owner != null && owner.getExecutable() instanceof Run) {
runReport = ((Run) owner.getExecutable()).getAction(EnvironmentWatcherRunReport.class);
}
}
if (runReport != null) {
return new EnvironmentWatcher(contextEnvVars, contextExpander);
}
}
} catch (InterruptedException | IOException e) {
LOGGER.log(Level.FINE, "Unable to create EnvironmentWatcher instance.\n" + e.getMessage());
}
return null;
}

public EnvironmentWatcher(@Nonnull EnvVars envVars, @Nonnull EnvironmentExpander expander) {
this.envVars = envVars;
watchedVars = expander.getSensitiveVars();
}

public void scan(String text) {
scanResults = watchedVars.stream().filter(e -> text.contains(envVars.get(e))).collect(Collectors.toList());
}

public void logResults(TaskListener listener) {
if (scanResults != null && !scanResults.isEmpty()) {
listener.getLogger().println("The following Groovy string may be insecure. Use single quotes to prevent leaking secrets via Groovy interpolation. Affected variables: " + scanResults.toString());
runReport.record(scanResults);
}
}

private static final long serialVersionUID = 1L;
}
Loading