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

allow import of Groovy code from the workspace #1078

Merged
merged 1 commit into from
Jan 16, 2018
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
2 changes: 2 additions & 0 deletions docs/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Browse the Jenkins issue tracker to see any [open issues](https://issues.jenkins

## Release Notes
* 1.67 (unreleased)
* Allow import of Groovy code from the workspace when script security sandbox is enabled
([#1078](https://github.com/jenkinsci/job-dsl-plugin/pull/1078))
* Enhanced support for the [Groovy Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Groovy+plugin)
([JENKINS-44256](https://issues.jenkins-ci.org/browse/JENKINS-44256))
* Support for the older versions of the [Groovy Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Groovy+plugin) is
Expand Down
8 changes: 1 addition & 7 deletions docs/Real-World-Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,6 @@ REST API calls
Import other files (i.e. with class definitions) into your script
-----------------------------------------------------------------

> Importing Groovy classes from the workspace is not possible when script security is enabled since that would undermine
> the script approval process. As an alternative it is possible to package the classes into a JAR file and add that JAR
> to the classpath through the _Additional classpath_ option. Classpath entries are subject to the approval process. See
> [Job DSL Gradle Example](https://github.com/sheehan/job-dsl-gradle-example) or
> [Job DSL Sample](https://github.com/unguiculus/job-dsl-sample) as starting point for building and packaging classes.

Make a directory at the same level as the DSL called `utilities` and create a file called `MyUtilities.groovy` in the
`utilities` directory with the following contents:

Expand All @@ -92,4 +86,4 @@ Then from the DSL, add something like this:
def myJob = job('example')
MyUtilities.addMyFeature(myJob)

Note that importing other files is not possible when [[Script Security]] is enabled.
Note that importing other files is not possible when [[Script Security]] is enabled and not using Groovy Sandbox.
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ abstract class AbstractDslScriptLoader<S extends JobParent, G extends GeneratedI

GroovyShell groovyShell = groovyShellCache[key]
if (!groovyShell) {
ClassLoader classLoader = prepareClassLoader(AbstractDslScriptLoader.classLoader)
groovyShell = new GroovyShell(
new URLClassLoader(scriptRequest.urlRoots, classLoader),
prepareClassLoader(scriptRequest.urlRoots, AbstractDslScriptLoader.classLoader),
new Binding(),
config
)
Expand All @@ -65,8 +64,13 @@ abstract class AbstractDslScriptLoader<S extends JobParent, G extends GeneratedI
}
} finally {
groovyShellCache.values().each { GroovyShell groovyShell ->
groovyShell.classLoader.close()
groovyShell.classLoader.parent.close()
ClassLoader classLoader = groovyShell.classLoader
while (classLoader != AbstractDslScriptLoader.classLoader) {
if (classLoader instanceof Closeable) {
((Closeable) classLoader).close()
}
classLoader = classLoader.parent
}
}
}
generatedItems
Expand Down Expand Up @@ -112,8 +116,11 @@ abstract class AbstractDslScriptLoader<S extends JobParent, G extends GeneratedI
}
}

protected ClassLoader prepareClassLoader(ClassLoader classLoader) {
classLoader
/**
* @since 1.67
*/
protected ClassLoader prepareClassLoader(URL[] urlRoots, ClassLoader classLoader) {
new URLClassLoader(urlRoots, classLoader)
}

protected GroovyCodeSource createGroovyCodeSource(ScriptRequest scriptRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import hudson.model.Item
import hudson.security.ACL
import javaposse.jobdsl.dsl.DslException
import javaposse.jobdsl.dsl.JobManagement
import javaposse.jobdsl.dsl.ScriptRequest
import jenkins.model.Jenkins
import org.acegisecurity.AccessDeniedException
import org.codehaus.groovy.control.CompilerConfiguration
Expand All @@ -28,8 +29,15 @@ class SandboxDslScriptLoader extends SecureDslScriptLoader {
}

@Override
protected ClassLoader prepareClassLoader(ClassLoader classLoader) {
GroovySandbox.createSecureClassLoader(classLoader)
protected ClassLoader prepareClassLoader(URL[] urlRoots, ClassLoader classLoader) {
GroovySandbox.createSecureClassLoader(new WorkspaceClassLoader(urlRoots[0], classLoader, seedJob))
}

protected Collection<ScriptRequest> createSecureScriptRequests(Collection<ScriptRequest> scriptRequests) {
scriptRequests.collect {
// it is not safe to use additional classpath entries
new ScriptRequest(it.body, it.urlRoots[0..0] as URL[], it.ignoreExisting, it.scriptPath)
}
}

@Override
Expand All @@ -46,4 +54,36 @@ class SandboxDslScriptLoader extends SecureDslScriptLoader {
throw new DslException(e.message, e)
}
}

private static class WorkspaceClassLoader extends URLClassLoader {
private final Item seedJob

WorkspaceClassLoader(URL workspaceUrl, ClassLoader parent, Item seedJob) {
super([workspaceUrl] as URL[], parent)
this.seedJob = seedJob
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name)
}

@Override
URL findResource(String name) {
if (!seedJob.hasPermission(Item.WORKSPACE)) {
return null
}

super.findResource(name)
}

@Override
Enumeration<URL> findResources(String name) throws IOException {
if (!seedJob.hasPermission(Item.WORKSPACE)) {
return Collections.emptyEnumeration()
}

super.findResources(name)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@ class ScriptApprovalDslScriptLoader extends SecureDslScriptLoader {
}

protected Collection<ScriptRequest> createSecureScriptRequests(Collection<ScriptRequest> scriptRequests) {
super.createSecureScriptRequests(scriptRequests).each {
scriptRequests.collect {
if (it.body) {
ScriptApproval.get().configuring(
it.body,
GroovyLanguage.get(),
ApprovalContext.create().withItem(seedJob)
)
}

// it is not safe to use additional classpath entries
new ScriptRequest(it.body, new URL[0], it.ignoreExisting, it.scriptPath)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ abstract class SecureDslScriptLoader extends JenkinsDslScriptLoader {
super.runScripts(createSecureScriptRequests(scriptRequests))
}

protected Collection<ScriptRequest> createSecureScriptRequests(Collection<ScriptRequest> scriptRequests) {
scriptRequests.collect {
// it is not safe to use additional classpath entries
new ScriptRequest(it.body, new URL[0], it.ignoreExisting, it.scriptPath)
}
@Override
protected ClassLoader prepareClassLoader(URL[] urlRoots, ClassLoader classLoader) {
new URLClassLoader([] as URL[], classLoader)
}

protected abstract Collection<ScriptRequest> createSecureScriptRequests(Collection<ScriptRequest> scriptRequests)
}
5 changes: 5 additions & 0 deletions job-dsl-plugin/src/test/groovy/ScriptHelper.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ScriptHelper {
static foo() {
'foo'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,123 @@ class ExecuteDslScriptsSpec extends Specification {
ScriptApproval.get().pendingSignatures*.signature == ['staticMethod java.lang.System exit int']
}

def 'run script in sandbox with import from workspace'() {
setup:
String script = 'import Helper\njob(Helper.computeName()) { description("foo") }'

jenkinsRule.instance.securityRealm = jenkinsRule.createDummySecurityRealm()
jenkinsRule.instance.authorizationStrategy = new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE, Item.CREATE, Computer.BUILD, Item.WORKSPACE)
.everywhere().to('dev')

FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')
FreeStyleBuild build = job.scheduleBuild2(0).get()
build.workspace.child('Helper.groovy').write('class Helper { static computeName() { "foo" } }', 'UTF-8')
job.buildersList.add(new ExecuteDslScripts(scriptText: script, sandbox: true))
setupQIA('dev', job)

when:
jenkinsRule.submit(jenkinsRule.createWebClient().login('dev').getPage(job, 'configure').getFormByName('config'))

then:
assert ScriptApproval.get().pendingScripts*.script == []

when:
build = job.scheduleBuild2(0).get()

then:
build.result == SUCCESS
assert ScriptApproval.get().pendingScripts*.script == []
}

def 'cannot run script in sandbox with import from workspace without WORKSPACE permission'() {
setup:
String script = 'import Helper\njob(Helper.computeName()) { description("foo") }'

jenkinsRule.instance.securityRealm = jenkinsRule.createDummySecurityRealm()
jenkinsRule.instance.authorizationStrategy = new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE, Item.CREATE, Computer.BUILD).everywhere().to('dev')

FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')
FreeStyleBuild build = job.scheduleBuild2(0).get()
build.workspace.child('Helper.groovy').write('class Helper { static computeName() { "foo" } }', 'UTF-8')
job.buildersList.add(new ExecuteDslScripts(scriptText: script, sandbox: true))
setupQIA('dev', job)

when:
jenkinsRule.submit(jenkinsRule.createWebClient().login('dev').getPage(job, 'configure').getFormByName('config'))

then:
assert ScriptApproval.get().pendingScripts*.script == []

when:
build = job.scheduleBuild2(0).get()

then:
build.result == FAILURE
build.log.contains('unable to resolve class Helper')
ScriptApproval.get().pendingSignatures.isEmpty()
}

def 'run script in sandbox with import from workspace with unapproved signature'() {
setup:
String script = 'import Helper\njob(Helper.boom()) { description("foo") }'

jenkinsRule.instance.securityRealm = jenkinsRule.createDummySecurityRealm()
jenkinsRule.instance.authorizationStrategy = new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE, Item.CREATE, Computer.BUILD, Item.WORKSPACE)
.everywhere().to('dev')

FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')
FreeStyleBuild build = job.scheduleBuild2(0).get()
build.workspace.child('Helper.groovy').write('class Helper { static boom() { System.exit(0) } }', 'UTF-8')
job.buildersList.add(new ExecuteDslScripts(scriptText: script, sandbox: true))
setupQIA('dev', job)

when:
jenkinsRule.submit(jenkinsRule.createWebClient().login('dev').getPage(job, 'configure').getFormByName('config'))

then:
assert ScriptApproval.get().pendingScripts*.script == []

when:
build = job.scheduleBuild2(0).get()

then:
build.result == FAILURE
ScriptApproval.get().pendingSignatures*.signature == ['staticMethod java.lang.System exit int']
}

def 'cannot import compiled class from workspace'() {
setup:
String script = 'import ScriptHelper\njob(ScriptHelper.foo()) { description("foo") }'

jenkinsRule.instance.securityRealm = jenkinsRule.createDummySecurityRealm()
jenkinsRule.instance.authorizationStrategy = new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE, Item.CREATE, Computer.BUILD, Item.WORKSPACE)
.everywhere().to('dev')

FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')
FreeStyleBuild build = job.scheduleBuild2(0).get()
build.workspace.child('ScriptHelper.class').copyFrom(getClass().getResourceAsStream('/ScriptHelper.class'))
job.buildersList.add(new ExecuteDslScripts(scriptText: script, sandbox: true))
setupQIA('dev', job)

when:
jenkinsRule.submit(jenkinsRule.createWebClient().login('dev').getPage(job, 'configure').getFormByName('config'))

then:
assert ScriptApproval.get().pendingScripts*.script == []

when:
build = job.scheduleBuild2(0).get()

then:
build.result == FAILURE
build.log.contains('Scripts not permitted to use staticMethod ScriptHelper foo')
ScriptApproval.get().pendingSignatures*.signature == ['staticMethod ScriptHelper foo']
}

def 'cannot run script in sandbox without queue item authentication'() {
setup:
String script = 'job("test") { description("foo") }'
Expand Down