Skip to content

Commit

Permalink
Merge pull request #10 from jglick/readTrusted-JENKINS-34596
Browse files Browse the repository at this point in the history
[JENKINS-34596] Added readTrusted step
  • Loading branch information
jglick committed Jun 2, 2016
2 parents c2a40fe + 3efa77b commit 830a4e4
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 1 deletion.
@@ -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";
}

}

}
@@ -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.
@@ -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>
@@ -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>
@@ -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>
@@ -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);
}

}

0 comments on commit 830a4e4

Please sign in to comment.