Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from jglick/readTrusted-JENKINS-34596
[JENKINS-34596] Added readTrusted step
- Loading branch information
Showing
7 changed files
with
393 additions
and
1 deletion.
There are no files selected for viewing
188 changes: 188 additions & 0 deletions
188
src/main/java/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2016 CloudBees, Inc. | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package org.jenkinsci.plugins.workflow.multibranch; | ||
|
||
import hudson.AbortException; | ||
import hudson.Extension; | ||
import hudson.FilePath; | ||
import hudson.model.Computer; | ||
import hudson.model.ItemGroup; | ||
import hudson.model.Job; | ||
import hudson.model.Node; | ||
import hudson.model.Run; | ||
import hudson.model.TaskListener; | ||
import hudson.model.TopLevelItem; | ||
import hudson.slaves.WorkspaceList; | ||
import java.io.IOException; | ||
import javax.inject.Inject; | ||
import jenkins.branch.Branch; | ||
import jenkins.model.Jenkins; | ||
import jenkins.scm.api.SCMHead; | ||
import jenkins.scm.api.SCMRevision; | ||
import jenkins.scm.api.SCMRevisionAction; | ||
import jenkins.scm.api.SCMSource; | ||
import org.jenkinsci.plugins.workflow.cps.steps.LoadStepExecution; | ||
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; | ||
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; | ||
import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; | ||
import org.jenkinsci.plugins.workflow.steps.StepContextParameter; | ||
import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep; | ||
import org.jenkinsci.plugins.workflow.steps.scm.SCMStep; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
|
||
/** | ||
* Replacement for {@code readFile} which reads from the SCM using {@link SCMSource#getTrustedRevision}. | ||
* Refuses to load a file which has been modified in an untrusted revision. | ||
* If run multiple times, always loads from the same revision. | ||
* May be used in combination with {@code evaluate} to delegate to more Pipeline Groovy, as a substitute for {@link SCMBinder}, | ||
* at least until {@link LoadStepExecution} has been split into an abstract part that a {@code loadTrusted} step could extend. | ||
*/ | ||
public class ReadTrustedStep extends AbstractStepImpl { | ||
|
||
private final String path; | ||
// TODO encoding | ||
|
||
@DataBoundConstructor public ReadTrustedStep(String path) { | ||
this.path = path; | ||
} | ||
|
||
public String getPath() { | ||
return path; | ||
} | ||
|
||
public static class Execution extends AbstractSynchronousStepExecution<String> { | ||
|
||
@Inject private transient ReadTrustedStep step; | ||
@StepContextParameter private transient Run<?,?> build; | ||
@StepContextParameter private transient TaskListener listener; | ||
|
||
@Override protected String run() throws Exception { | ||
// Adapted from SCMBinder: | ||
Job<?,?> job = build.getParent(); | ||
BranchJobProperty property = job.getProperty(BranchJobProperty.class); | ||
if (property == null) { | ||
throw new IllegalStateException("inappropriate context"); | ||
} | ||
Branch branch = property.getBranch(); | ||
ItemGroup<?> parent = job.getParent(); | ||
if (!(parent instanceof WorkflowMultiBranchProject)) { | ||
throw new IllegalStateException("inappropriate context"); | ||
} | ||
SCMSource scmSource = ((WorkflowMultiBranchProject) parent).getSCMSource(branch.getSourceId()); | ||
if (scmSource == null) { | ||
throw new IllegalStateException(branch.getSourceId() + " not found"); | ||
} | ||
SCMHead head = branch.getHead(); | ||
SCMRevision tip; | ||
SCMRevisionAction action = build.getAction(SCMRevisionAction.class); | ||
if (action != null) { | ||
tip = action.getRevision(); | ||
} else { | ||
tip = scmSource.fetch(head, listener); | ||
if (tip == null) { | ||
throw new AbortException("Could not determine exact tip revision of " + branch.getName()); | ||
} | ||
build.addAction(new SCMRevisionAction(tip)); | ||
} | ||
SCMRevision trusted = scmSource.getTrustedRevision(tip, listener); | ||
// Adapted from CpsScmFlowDefinition: | ||
Node node = Jenkins.getActiveInstance(); | ||
FilePath dir; | ||
if (job instanceof TopLevelItem) { | ||
FilePath baseWorkspace = node.getWorkspaceFor((TopLevelItem) job); | ||
if (baseWorkspace == null) { | ||
throw new AbortException(node.getDisplayName() + " may be offline"); | ||
} | ||
dir = getFilePathWithSuffix(baseWorkspace); | ||
} else { // should not happen, but just in case: | ||
throw new IllegalStateException(job + " was not top level"); | ||
} | ||
FilePath file = dir.child(step.path); | ||
if (!file.absolutize().getRemote().replace('\\', '/').startsWith(dir.absolutize().getRemote().replace('\\', '/') + '/')) { // TODO JENKINS-26838 | ||
throw new IOException(file + " is not inside " + dir); | ||
} | ||
Computer computer = node.toComputer(); | ||
if (computer == null) { | ||
throw new IOException(node.getDisplayName() + " may be offline"); | ||
} | ||
WorkspaceList.Lease lease = computer.getWorkspaceList().acquire(dir); | ||
try { | ||
String untrustedFile = null; | ||
if (!tip.equals(trusted)) { | ||
SCMStep delegate = new GenericSCMStep(scmSource.build(head, tip)); | ||
delegate.setPoll(false); | ||
delegate.setChangelog(false); | ||
delegate.checkout(build, dir, listener, node.createLauncher(listener)); | ||
if (!file.exists()) { | ||
throw new AbortException(file + " not found"); | ||
} | ||
untrustedFile = file.readToString(); | ||
} | ||
SCMStep delegate = new GenericSCMStep(scmSource.build(head, trusted)); | ||
delegate.setPoll(true); | ||
delegate.setChangelog(true); | ||
delegate.checkout(build, dir, listener, node.createLauncher(listener)); | ||
if (!file.exists()) { | ||
throw new AbortException(file + " not found"); | ||
} | ||
String content = file.readToString(); | ||
if (untrustedFile != null && !untrustedFile.equals(content)) { | ||
throw new AbortException(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis(step.path)); | ||
} | ||
return content; | ||
} finally { | ||
lease.release(); | ||
} | ||
} | ||
|
||
private FilePath getFilePathWithSuffix(FilePath baseWorkspace) { | ||
return baseWorkspace.withSuffix(getFilePathSuffix() + "script"); | ||
} | ||
|
||
private String getFilePathSuffix() { | ||
return System.getProperty(WorkspaceList.class.getName(), "@"); | ||
} | ||
|
||
private static final long serialVersionUID = 1L; | ||
|
||
} | ||
|
||
@Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { | ||
|
||
public DescriptorImpl() { | ||
super(Execution.class); | ||
} | ||
|
||
@Override public String getFunctionName() { | ||
return "readTrusted"; | ||
} | ||
|
||
@Override public String getDisplayName() { | ||
return "Read trusted file from SCM"; | ||
} | ||
|
||
} | ||
|
||
} |
3 changes: 2 additions & 1 deletion
3
src/main/resources/org/jenkinsci/plugins/workflow/multibranch/Messages.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
ReadTrustedStep._has_been_modified_in_an_untrusted_revis=\u2018{0}\u2019 has been modified in an untrusted revision | ||
WorkflowMultiBranchProject.DisplayName=Multibranch Pipeline | ||
WorkflowMultiBranchProject.Description=Creates a set of Pipeline projects according to detected branches in one SCM repository. | ||
WorkflowMultiBranchProject.Description=Creates a set of Pipeline projects according to detected branches in one SCM repository. |
31 changes: 31 additions & 0 deletions
31
src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/config.jelly
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
The MIT License | ||
Copyright 2016 CloudBees, Inc. | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. | ||
--> | ||
|
||
<?jelly escape-by-default='true'?> | ||
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> | ||
<f:entry field="path" title="${Path}"> | ||
<f:textbox/> | ||
</f:entry> | ||
</j:jelly> |
4 changes: 4 additions & 0 deletions
4
src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/help-path.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<div> | ||
Relative (slash-separated) path to the file from the SCM root. | ||
Thus <code>readTrusted 'subdir/file'</code> is similar to <code>node {checkout scm; readFile 'subdir/file'}</code>. | ||
</div> |
6 changes: 6 additions & 0 deletions
6
src/main/resources/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStep/help.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<div> | ||
From a multibranch Pipeline project, reads a file from the associated SCM and returns its contents. | ||
Unlike the <code>readFile</code> step, no workspace is required. | ||
If the associated branch is not trusted, yet the file has been modified from its trusted version, an error is thrown. | ||
Thus this step is useful for loading scripts or other files which might otherwise be used to run malicious commands. | ||
</div> |
156 changes: 156 additions & 0 deletions
156
src/test/java/org/jenkinsci/plugins/workflow/multibranch/ReadTrustedStepTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2016 CloudBees, Inc. | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package org.jenkinsci.plugins.workflow.multibranch; | ||
|
||
import hudson.model.Result; | ||
import jenkins.branch.BranchProperty; | ||
import jenkins.branch.BranchSource; | ||
import jenkins.branch.DefaultBranchPropertyStrategy; | ||
import org.jenkinsci.plugins.workflow.job.WorkflowJob; | ||
import org.jenkinsci.plugins.workflow.job.WorkflowRun; | ||
import org.jenkinsci.plugins.workflow.steps.scm.GitSampleRepoRule; | ||
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; | ||
import org.junit.Test; | ||
import static org.junit.Assert.*; | ||
import org.junit.ClassRule; | ||
import org.junit.Rule; | ||
import org.jvnet.hudson.test.BuildWatcher; | ||
import org.jvnet.hudson.test.JenkinsRule; | ||
|
||
public class ReadTrustedStepTest { | ||
|
||
@ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); | ||
@Rule public JenkinsRule r = new JenkinsRule(); | ||
@Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); | ||
|
||
@Test public void smokes() throws Exception { | ||
sampleRepo.init(); | ||
sampleRepo.write("Jenkinsfile", "echo \"said ${readTrusted 'message'}\""); | ||
sampleRepo.write("message", "how do you do"); | ||
sampleRepo.git("add", "Jenkinsfile", "message"); | ||
sampleRepo.git("commit", "--all", "--message=defined"); | ||
WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); | ||
mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); | ||
WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); | ||
r.waitUntilNoActivity(); | ||
WorkflowRun b = p.getLastBuild(); | ||
assertNotNull(b); | ||
assertEquals(1, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
r.assertBuildStatusSuccess(b); | ||
r.assertLogContains("said how do you do", b); | ||
String branch = "evil"; | ||
sampleRepo.git("checkout", "-b", branch); | ||
sampleRepo.write("message", "your father smelt of elderberries"); | ||
sampleRepo.git("commit", "--all", "--message=rude"); | ||
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch); | ||
r.waitUntilNoActivity(); | ||
b = p.getLastBuild(); | ||
assertNotNull(b); | ||
assertEquals(1, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
r.assertBuildStatus(Result.FAILURE, b); | ||
r.assertLogContains(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis("message"), b); | ||
sampleRepo.write("message", "how do you do"); | ||
sampleRepo.write("ignored-message", "I fart in your general direction"); | ||
sampleRepo.git("add", "ignored-message"); | ||
sampleRepo.git("commit", "--all", "--message=less rude"); | ||
sampleRepo.notifyCommit(r); | ||
b = p.getLastBuild(); | ||
assertEquals(2, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
r.assertBuildStatusSuccess(b); | ||
r.assertLogContains("said how do you do", b); | ||
} | ||
|
||
@Test public void exactRevision() throws Exception { | ||
sampleRepo.init(); | ||
sampleRepo.write("Jenkinsfile", "node {checkout scm; semaphore 'wait1'; def alpha = readTrusted 'alpha'; semaphore 'wait2'; echo \"first got ${alpha} then ${readTrusted 'beta'} vs. disk ${readFile 'alpha'} then ${readFile 'beta'}\"}"); | ||
sampleRepo.write("alpha", "1"); | ||
sampleRepo.write("beta", "1"); | ||
sampleRepo.git("add", "Jenkinsfile", "alpha", "beta"); | ||
sampleRepo.git("commit", "--all", "--message=defined"); | ||
WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); | ||
mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); | ||
WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); | ||
SemaphoreStep.waitForStart("wait1/1", null); | ||
WorkflowRun b = p.getLastBuild(); | ||
assertNotNull(b); | ||
assertEquals(1, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
sampleRepo.write("alpha", "2"); | ||
sampleRepo.git("commit", "--all", "--message=alpha-2"); | ||
SemaphoreStep.success("wait1/1", null); | ||
SemaphoreStep.waitForStart("wait2/1", b); | ||
sampleRepo.write("beta", "2"); | ||
sampleRepo.git("commit", "--all", "--message=beta-2"); | ||
SemaphoreStep.success("wait2/1", null); | ||
r.assertLogContains("first got 1 then 1 vs. disk 1 then 1", r.assertBuildStatusSuccess(r.waitForCompletion(b))); | ||
sampleRepo.write("Jenkinsfile", "def alpha = readTrusted 'alpha'; semaphore 'wait1'; node {checkout scm; semaphore 'wait2'; echo \"now got ${alpha} then ${readTrusted 'beta'} vs. disk ${readFile 'alpha'} then ${readFile 'beta'}\"}"); | ||
sampleRepo.git("commit", "--all", "--message=new definition"); | ||
b = p.scheduleBuild2(0).waitForStart(); | ||
SemaphoreStep.waitForStart("wait1/2", b); | ||
sampleRepo.write("alpha", "3"); | ||
sampleRepo.git("commit", "--all", "--message=alpha-3"); | ||
SemaphoreStep.success("wait1/2", null); | ||
SemaphoreStep.waitForStart("wait2/2", b); | ||
sampleRepo.write("beta", "3"); | ||
sampleRepo.git("commit", "--all", "--message=beta-3"); | ||
SemaphoreStep.success("wait2/2", null); | ||
r.assertLogContains("now got 2 then 2 vs. disk 2 then 2", r.assertBuildStatusSuccess(r.waitForCompletion(b))); | ||
} | ||
|
||
@Test public void evaluate() throws Exception { | ||
sampleRepo.init(); | ||
sampleRepo.write("Jenkinsfile", "evaluate readTrusted('lib.groovy')"); | ||
sampleRepo.write("lib.groovy", "echo 'trustworthy library'"); | ||
sampleRepo.git("add", "Jenkinsfile", "lib.groovy"); | ||
sampleRepo.git("commit", "--all", "--message=defined"); | ||
WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); | ||
mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false), new DefaultBranchPropertyStrategy(new BranchProperty[0]))); | ||
WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); | ||
r.waitUntilNoActivity(); | ||
WorkflowRun b = p.getLastBuild(); | ||
assertNotNull(b); | ||
assertEquals(1, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
r.assertBuildStatusSuccess(b); | ||
r.assertLogContains("trustworthy library", b); | ||
String branch = "evil"; | ||
sampleRepo.git("checkout", "-b", branch); | ||
sampleRepo.write("lib.groovy", "echo 'not trustworthy'"); | ||
sampleRepo.git("commit", "--all", "--message=evil"); | ||
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch); | ||
r.waitUntilNoActivity(); | ||
b = p.getLastBuild(); | ||
assertNotNull(b); | ||
assertEquals(1, b.getNumber()); | ||
SCMBinderTest.assertRevisionAction(b); | ||
r.assertBuildStatus(Result.FAILURE, b); | ||
r.assertLogContains(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis("lib.groovy"), b); | ||
r.assertLogNotContains("not trustworthy", b); | ||
} | ||
|
||
} |
Oops, something went wrong.