Skip to content
Permalink
Browse files

[JENKINS-22834] Add a feature to specify additional class paths.

  • Loading branch information
ikedam committed Jun 16, 2014
1 parent ccd8c92 commit 1bd2137f24d27a24083b021ed6432d04328edab4
@@ -0,0 +1,80 @@
/*
* The MIT License
*
* Copyright (c) 2014 IKEDA Yasuyuki
*
* 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.scriptsecurity.sandbox.groovy;

import java.io.File;

import org.kohsuke.stapler.DataBoundConstructor;

import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;

/**
*
*/
public class AdditionalClasspath extends AbstractDescribableImpl<AdditionalClasspath> {
private final String path;

@DataBoundConstructor
public AdditionalClasspath(String path) {
this.path = path;
}

public String getPath() {
return path;
}

public File getClasspath() {
return new File(getPath());
}

@Override
public String toString() {
return String.format("Classpath: %s", getPath());
}

@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof AdditionalClasspath)) {
return false;
}

if (getPath() == null) {
return ((AdditionalClasspath)obj).getPath() == null;
}

return getPath().equals(((AdditionalClasspath)obj).getPath());
}

@Extension
public static class DescriptorImpl extends Descriptor<AdditionalClasspath> {
@Override
public String getDisplayName() {
// TODO
return "classpath";
}
}
}
@@ -29,9 +29,19 @@
import hudson.Extension;
import hudson.PluginManager;
import hudson.model.AbstractDescribableImpl;
import hudson.model.BuildListener;
import hudson.model.StreamBuildListener;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.util.FormValidation;
import hudson.util.NullStream;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import javax.annotation.CheckForNull;
import jenkins.model.Jenkins;
@@ -57,11 +67,17 @@

private final String script;
private final boolean sandbox;
private final List<AdditionalClasspath> additionalClasspathList;
private transient boolean calledConfiguring;

@DataBoundConstructor public SecureGroovyScript(String script, boolean sandbox) {
@DataBoundConstructor public SecureGroovyScript(String script, boolean sandbox, List<AdditionalClasspath> additionalClasspathList) {
this.script = script;
this.sandbox = sandbox;
this.additionalClasspathList = additionalClasspathList;
}

public SecureGroovyScript(String script, boolean sandbox) {
this(script, sandbox, null);
}

private Object readResolve() {
@@ -77,6 +93,10 @@ public boolean isSandbox() {
return sandbox;
}

public List<AdditionalClasspath> getAdditionalClasspathList() {
return additionalClasspathList;
}

/**
* To be called in your own {@link DataBoundConstructor} when storing the field of this type.
* Should always be called, though it does nothing when {@link #isSandbox}.
@@ -124,10 +144,35 @@ public SecureGroovyScript configuringWithNonKeyItem() {
* @throws RejectedAccessException in case of a sandbox issue
* @throws UnapprovedUsageException in case of a non-sandbox issue
*/
public Object evaluate(ClassLoader loader, Binding binding) throws Exception {
public Object evaluate(BuildListener listener, ClassLoader loader, Binding binding) throws Exception {
if (!calledConfiguring) {
throw new IllegalStateException("you need to call configuring or a related method before using GroovyScript");
}
if (getAdditionalClasspathList() != null && !getAdditionalClasspathList().isEmpty()) {
// TODO check approval of classpath
List<URL> urlList = new ArrayList<URL>(getAdditionalClasspathList().size());

for (AdditionalClasspath classpath: getAdditionalClasspathList()) {
File file = classpath.getClasspath();
if (!file.isAbsolute()) {
listener.getLogger().println(String.format("%s: classpath should be absolute. Not added to class loader", file));
continue;
}
if (!file.exists()) {
listener.getLogger().println(String.format("%s: Does not exist. Not added to class loader", file));
continue;
}
try {
urlList.add(file.toURI().toURL());
} catch (MalformedURLException e) {
listener.getLogger().println(String.format("%s: Bad url. Not added to class loader", file));
e.printStackTrace(listener.getLogger());
continue;
}
}

loader = new URLClassLoader(urlList.toArray(new URL[0]), loader);
}
if (sandbox) {
GroovyShell shell = new GroovyShell(loader, binding, GroovySandbox.createSecureCompilerConfiguration());
try {
@@ -140,6 +185,11 @@ public Object evaluate(ClassLoader loader, Binding binding) throws Exception {
}
}

@Deprecated
public Object evaluate(ClassLoader loader, Binding binding) throws Exception {
return evaluate(new StreamBuildListener(new NullStream()), loader, binding);
}

@Extension public static final class DescriptorImpl extends Descriptor<SecureGroovyScript> {

@Override public String getDisplayName() {
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright (c) 2014 IKEDA Yasuyuki
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 to Jarfile}">
<f:textbox />
</f:entry>
<f:entry title="">
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:entry>
</j:jelly>
@@ -33,4 +33,13 @@ THE SOFTWARE.
<f:entry field="sandbox" title="${%Use Groovy Sandbox}">
<f:checkbox/>
</f:entry>
<f:advanced>
<f:entry title="${%Additional classpath}" field="additionalClasspathList">
<f:repeatableProperty
field="additionalClasspathList"
add="${%Add classpath}"
header="${%Classpath}"
/>
</f:entry>
</f:advanced>
</j:jelly>
@@ -28,15 +28,21 @@
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
import hudson.model.FreeStyleProject;
import hudson.model.FreeStyleBuild;
import hudson.model.Item;
import hudson.model.Result;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.Permission;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;
import java.io.File;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.DirectoryScanner;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
import static org.junit.Assert.*;
@@ -98,4 +104,106 @@
assertEquals("P#3", r.assertBuildStatusSuccess(p.scheduleBuild2(0)).getDescription());
}

private List<File> getAllJarFiles() throws URISyntaxException {
String testClassPath = String.format(StringUtils.join(getClass().getName().split("\\."), "/"));
File testClassDir = new File(ClassLoader.getSystemResource(testClassPath).toURI()).getAbsoluteFile();

DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(testClassDir);
ds.setIncludes(new String[]{ "**/*.jar" });
ds.scan();

List<File> ret = new ArrayList<File>();

for (String relpath: ds.getIncludedFiles()) {
ret.add(new File(testClassDir, relpath));
}

return ret;
}

@Test public void testClasspathConfiguration() throws Exception {
List<AdditionalClasspath> classpathList = new ArrayList<AdditionalClasspath>();
for (File jarfile: getAllJarFiles()) {
classpathList.add(new AdditionalClasspath(jarfile.getAbsolutePath()));
}

FreeStyleProject p = r.createFreeStyleProject();
p.getPublishersList().add(new TestGroovyRecorder(new SecureGroovyScript(
"whatever",
true,
classpathList
)));

JenkinsRule.WebClient wc = r.createWebClient();
r.submit(wc.getPage(p, "configure").getFormByName("config"));

p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class);
TestGroovyRecorder recorder = (TestGroovyRecorder)p.getPublishersList().get(0);
assertEquals(classpathList, recorder.getScript().getAdditionalClasspathList());
}

@Test public void testClasspathInSandbox() throws Exception {
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
GlobalMatrixAuthorizationStrategy gmas = new GlobalMatrixAuthorizationStrategy();
gmas.add(Jenkins.READ, "devel");
for (Permission p : Item.PERMISSIONS.getPermissions()) {
gmas.add(p, "devel");
}
r.jenkins.setAuthorizationStrategy(gmas);

List<AdditionalClasspath> classpathList = new ArrayList<AdditionalClasspath>();
for (File jarfile: getAllJarFiles()) {
classpathList.add(new AdditionalClasspath(jarfile.getAbsolutePath()));
}

final String testingDisplayName = "TESTDISPLAYNAME";

{
FreeStyleProject p = r.createFreeStyleProject();
p.getPublishersList().add(new TestGroovyRecorder(new SecureGroovyScript(
String.format("build.setDisplayName(\"%s\"); \"\";", testingDisplayName),
true,
classpathList
)));

FreeStyleBuild b = p.scheduleBuild2(0).get();
// fails for accessing non-whitelisted method.
r.assertBuildStatus(Result.FAILURE, b);
assertNotEquals(testingDisplayName, b.getDisplayName());
}

{
FreeStyleProject p = r.createFreeStyleProject();
p.getPublishersList().add(new TestGroovyRecorder(new SecureGroovyScript(
String.format(
"import org.jenkinsci.plugins.scriptsecurity.testjar.BuildUtil;"
+ "BuildUtil.setDisplayName(build, \"%s\")"
+ "\"\"", testingDisplayName),
true,
classpathList
)));

FreeStyleBuild b = p.scheduleBuild2(0).get();
// fails for accessing non-whitelisted method.
r.assertBuildStatus(Result.FAILURE, b);
assertNotEquals(testingDisplayName, b.getDisplayName());
}

{
FreeStyleProject p = r.createFreeStyleProject();
p.getPublishersList().add(new TestGroovyRecorder(new SecureGroovyScript(
String.format(
"import org.jenkinsci.plugins.scriptsecurity.testjar.BuildUtil;"
+ "BuildUtil.setDisplayNameWhitelisted(build, \"%s\");"
+ "\"\"", testingDisplayName),
true,
classpathList
)));

FreeStyleBuild b = p.scheduleBuild2(0).get();
r.assertBuildStatusSuccess(b);
assertEquals(testingDisplayName, b.getDisplayName());
}
}
}
@@ -0,0 +1 @@
libraries in this directory are from https://github.com/ikedam/script-security-plugin-testjar.

0 comments on commit 1bd2137

Please sign in to comment.
You can’t perform that action at this time.