How to disallow dependencies via #Grab in Groovy script - java

I want to allow users to run their Groovy scripts in my Java server app, but I also want disallow them to use #Grab for adding any random dependencies.
Yes, I can simply cut off all #Grab annotations by search & replace in source code, but it will be better to do this in more elegant way, e.g. allow approved dependencies only.
And yes, I know that the best solution of this prob is JVM's SecurityManager.

There are various approaches, such as the Groovy Sandbox, which may work better than what you're about to see.
import groovy.grape.Grape
Grape.metaClass.static.grab = {String endorsed ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
Grape.metaClass.static.grab = {Map dependency ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
Grape.metaClass.static.grab = {Map args, Map dependency ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
def source1 = '''
println('This is a nice safe Groovy script.')
'''
def source2 = '''
#Grab('commons-validator:commons-validator:1.4.1')
import org.apache.commons.validator.routines.EmailValidator
def emailValidator = EmailValidator.getInstance();
assert emailValidator.isValid('what.a.shame#us.elections.gov')
assert !emailValidator.isValid('an_invalid_emai_address')
println 'You should not see this message!'
'''
def script
def shell = new GroovyShell()
try {
script = shell.parse(source1)
script.run()
} catch (Exception e) {
assert false, "Oh, oh. That wasn't supposed to happen :("
}
try {
script = shell.parse(source2)
assert false, "Oh, oh. That wasn't supposed to happen :("
} catch (ExceptionInInitializerError e) {
println 'Naughty script was blocked when parsed.'
}
The example above demonstrates how to block #Grab. It does this not by blocking the annotation, but by overriding the method call added by the annotation: groovy.grape.Grape.grab().
Grape.metaClass.static.grab = {String endorsed ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
Grape.metaClass.static.grab = {Map dependency ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
Grape.metaClass.static.grab = {Map args, Map dependency ->
throw new SecurityException("Oh no you didn't! Grabbing is forbidden.")
}
Here's the naughty script dissected by the Groovy Console AST viewer:
#groovy.lang.Grab(module = 'commons-validator', group = 'commons-validator', version = '1.4.1')
import org.apache.commons.validator.routines.EmailValidator as EmailValidator
public class script1440223706571 extends groovy.lang.Script {
private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo
public static transient boolean __$stMC
public script1440223706571() {
}
public script1440223706571(groovy.lang.Binding context) {
super(context)
}
public static void main(java.lang.String[] args) {
org.codehaus.groovy.runtime.InvokerHelper.runScript(script1440223706571, args)
}
public java.lang.Object run() {
java.lang.Object emailValidator = org.apache.commons.validator.routines.EmailValidator.getInstance()
assert emailValidator.isValid('what.a.shame#us.elections.gov') : null
assert !(emailValidator.isValid('an_invalid_emai_address')) : null
return null
}
static {
groovy.grape.Grape.grab([:], ['group': 'commons-validator', 'module': 'commons-validator', 'version': '1.4.1'])
}
protected groovy.lang.MetaClass $getStaticMetaClass() {
}
}
Here you can see the call to Grape.grab() in the static initializer. To add fine-grained filtering of dependencies, you can introspect the dependency and endorsed parameters.
dependency
['group': 'commons-validator', 'module': 'commons-validator', 'version': '1.4.1']
endorsed
commons-validator:commons-validator:1.4.1
Revised Implementation
This new implementation uses an Interceptor to block/allow Grape grabs.
import groovy.grape.GrapeIvy
def source1 = '''
println('This is a nice safe Groovy script.')
'''
def source2 = '''
#Grab('commons-validator:commons-validator:1.4.1')
import org.apache.commons.validator.routines.EmailValidator
def emailValidator = EmailValidator.getInstance();
assert emailValidator.isValid('what.a.shame#us.elections.gov')
assert !emailValidator.isValid('an_invalid_emai_address')
println 'You should not see this message!'
'''
def script
def shell = new GroovyShell()
def proxy = ProxyMetaClass.getInstance(GrapeIvy)
proxy.interceptor = new GrapeInterceptor({group, module, version ->
if(group == 'commons-validator' && module == 'commons-validator') false
else true
})
proxy.use {
shell.parse(source1).run()
try {
shell.parse(source2).run()
} catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) {
assert e.message.contains('unable to resolve class')
}
}
#groovy.transform.TupleConstructor
class GrapeInterceptor implements Interceptor {
private boolean invokeMethod = true
Closure authorizer
def afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
invokeMethod = true
return result
}
def beforeInvoke(Object object, String methodName, Object[] arguments) {
if(methodName == 'createGrabRecord') {
def dependencies = arguments[0]
invokeMethod = authorizer(dependencies.group, dependencies.module, dependencies.version)
} else {
invokeMethod = true
}
return null
}
boolean doInvoke() { invokeMethod }
}
The GrapeInterceptor constructor takes a Closure as its only argument. With this Closure you can easily decide whether to allow the Grab to occur or not :)
For example, if the Grab looks like this: #Grab('commons-validator:commons-validator:1.4.1')
The Closure's arguments would be assigned as follows:
group: commons-validator
module: commons-validator
version: 1.4.1
To allow the Grab, the Closure must return true. Return false to block it.

Related

How do you reference a function with the same name as a property?

Question
How can I reference this top-level function from within the data class? Or is Java's encapsulation of a class restrictive to the point that you cannot reach beyond the current class?
Code
def String branchName() {
return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1];
}
public DeployConfig implements IDeployConfig {
public DeployConfig(IDeployConfig config) {
this._appName = config.app;
this._gitUrl = config.gitUrl;
// ... et cetera
}
public String getBranchName() {
return branchName()
}
}
Background
I'm trying to define a data class that represents our standard Jenkinsfile configuration, in an attempt to make our pipeline more testable, and less "cross your fingers and hope it didn't break anything". Toward that goal, here is a snippet of that implementation.
Now, the property getter I'm trying to write doesn't know the actual branch being built when the object is constructed, because that's derived from the Map<String, String> returned by checkout scm which gets instantiated at runtime. We assign the GIT_BRANCH out to the global environment env.GIT_BRANCH so that it can be referenced elsewhere.
Miscellaneous
To the would-be suggestion of putting the target branch in the Jenkinsfile, that defeats the purpose of the Jenkinsfile being an instruction set for a job with Git configurations assigned, such as a multi-branch job with a shared Jenkinsfile.
Other Code
To give some context about what I mean about the checkout scm command happening after the construction of DeployConfig, the pipeline roughly resembles this:
// ./pipeline-library/vars/deploy.groovy
#!/usr/bin/groovy
def call(Closure body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
environmentVariables(config) // assigns certain keys to global env
if (env.IS_PROD) {
deployProd(config)
}
else {
deployNonProd(config)
}
}
// ./pipeline-library/vars/deployNonProd.groovy
#!/usr/bin/groovy
def call(Map config) {
// local variable declarations
pipeline {
agent {
label 'some-configuration-name'
}
environment {
// shared environment variables
}
options {
// configured options, like timestamps and log rotation
}
stages {
stage('Checkout') {
steps {
def gitInfo = checkout scm
env.GIT_BRANCH = gitInfo.GIT_BRANCH
}
}
// additional stages
}
}
}
Edits
Edit: The idea behind the property that calls the top-level function is a computed property that gets called later in the pipeline, after the checkout scm command has been executed. The DeployConfig would be constructed before the pipeline runs, and so the branch is not known at that time.
So I solved the problem for myself, but it's arguably a less than ideal solution to the problem. Basically, here's what I had to do:
First, I created a getter getGetBranchName and setter setGetBranchName on the class of type Closure<String> and it had a backing field _getBranchName. I also created a property getBranchName of type String that returned the result of this._getBranchName().
Second, if the incoming Map has a property branchName, then I set the value this._getBranchName = () -> { return config.branchName } so that I am referencing the getter of an outer object.
Third, as a final check, I assign the global function signature from Jenkins after constructing the DeployConfig object. That all looks like the below code (Note: ellipses are used to indicate more code unrelated to the specific solution):
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import com.domain.jenkins.data.DeployConfig
import com.domain.jenkins.data.GitUrl
import com.domain.jenkins.exceptions.InterruptException
import com.domain.jenkins.io.FileSystem
import com.domain.jenkins.pipeline.PipelineJob
import com.domain.jenkins.pipeline.PipelineStage
class Program {
static FileSystem fs
static PipelineJob pipeline
static Map<String, String> env
static Map jenkinsfile
static {
fs = new FileSystem()
pipeline = new PipelineJob()
env = [:]
jenkinsfile = [ ... ]
}
static String branchName() {
return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1]
}
static void main(String[] args) {
println 'Initialize pipeline'
pipeline = new PipelineJob()
println 'Initialize configuration'
DeployConfig config = new DeployConfig(jenkinsfile)
println new JsonBuilder(config.toMap()).toPrettyString()
println 'Assign static method as getBranchName method'
config.getBranchName = () -> { return branchName() }
println 'Assign environment variables to global env'
env << config.environmentVariables
...
}
}
And the DeployConfig class accepts that using the following (Note: most of the related code is not included for brevity's sake):
package com.domain.jenkins.data
import com.domain.jenkins.interfaces.IDeployConfig
import com.domain.jenkins.interfaces.IMappable
import com.domain.jenkins.interfaces.Mappable
class DeployConfig extends Mappable implements IDeployConfig, IMappable {
private Closure<String> _getBranchName
DeployConfig() {
this.branchName = 'master'
}
DeployConfig(Map options) {
this.branchName = options.branchName
}
String getBranchName() {
return this._getBranchName()
}
void setBranchName(String value) {
if(this._getBranchName == null) {
this._getBranchName = () -> { return 'master' }
}
if(value) {
this._getBranchName = () -> { return value }
}
}
void setGetBranchName(Closure<String> action) {
this._getBranchName = action
}
}

MissingPropertyException in Spock test

I have problem with test written in groovy (using Spock as a framework). While I try fetch property in then block and then use it in interaction I receive No such property: updatedValue for class: my.test.class.MyTestClass on the interaction with event publisher. Can anyone tell why is that?
class MyTestClass extends Specification {
#Autowired
private MyClass sut
#Autowired
private MyClassRepository myClassRepository
#SpringBean
private EventPublisher eventPublisher = Mock()
def "Should do something"() {
given:
//some data
def someId = "someId"
def event = new SomeEventObject()
when:
sut.handle(event)
then:
def updatedValue = myClassRepository.findTestClass(someId)
updatedTestClass.cash == 100
1 * eventPublisher.publishEvent(new SomeNextEvent(updatedValue))
}
}
You are encountering a side-effect of Spock's magic.
Your method basically gets transformed to this:
public void $spock_feature_0_0() {
org.spockframework.runtime.ErrorCollector $spock_errorCollector = org.spockframework.runtime.ErrorRethrower.INSTANCE
org.spockframework.runtime.ValueRecorder $spock_valueRecorder = new org.spockframework.runtime.ValueRecorder()
java.lang.Object someId = 'someId'
java.lang.Object event = new apackage.SomeEventObject()
this.getSpecificationContext().getMockController().enterScope()
this.getSpecificationContext().getMockController().addInteraction(new org.spockframework.mock.runtime.InteractionBuilder(25, 9, '1 * eventPublisher.publishEvent(new SomeNextEvent(updatedValue))').setFixedCount(1).addEqualTarget(eventPublisher).addEqualMethodName('publishEvent').setArgListKind(true, false).addEqualArg(new apackage.SomeNextEvent(updatedValue)).build())
sut.handle(event)
this.getSpecificationContext().getMockController().leaveScope()
java.lang.Object updatedValue = myClassRepository.findTestClass(someId)
try {
org.spockframework.runtime.SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), 'updatedTestClass.cash == 100', 23, 13, null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(3), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0), updatedTestClass).cash) == $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(2), 100)))
}
catch (java.lang.Throwable throwable) {
org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, 'updatedTestClass.cash == 100', 23, 13, null, throwable)}
finally {
}
this.getSpecificationContext().getMockController().leaveScope()
}
If you look closely, you can see that the interaction definition was moved before the when block, but the findTestClass() call stayed behind.
that is why you get the missing property exception.
The solution is to either move the lookup to the given block, or if that is not possible, to use argument capturing and then check afterwards.
given:
def capturedEvent
when:
...
then:
1 * eventPublisher.publishEvent(_) >> { capturedEvent = it[0} }
and:
def updatedValue = myClassRepository.findTestClass(someId)
capturedEvent instanceof SomeNextEvent
capturedEvent.value == updatedValue
You can take a look at the transformed code yourself in the groovy web console by clicking on Inspect AST.

Get JSON from Groovy object script with missing properties

I have a groovy script that may have some undefined properties:
// a new file say: test.groovy
import Comp;
if (a == null) { // a is a missing property
return new obj();
}
def objComp = new Comp(name: 'tim')
return objComp
I want to execute test.groovy and extract the JSON format of objComp using
new JsonBuilder(objComp).toPrettyString()
which can print something like
{
name: 'tim'
}
However I keep getting groovy.lang.MissingPropertyException because of the missing property a. I tried to use a separate class that extends Expando, but I get the same Exception:
class MyClass extends Expando {
def myMethod() {
def sharedData = new Binding()
def shell = new GroovyShell(sharedData)
result = shell.evaluate 'def test() { "eval test" }; return test()' // works fine
String fileContent = new File("/path/to/test.groovy").text
result = shell.evaluate(fileContent) // leads to MissingPropertyException
}
}
I was hoping using Expando would fix the MissingPropertyException, but it doesn't.

Gradle plugin extension set a property of type Map

I want to set a Map of attributes to my plugin extension. So basically I want to write something like
settings {
envVars = {
a = "abc"
b = "dec"
...
n = "sdf"
}
}
When I use an attribute in my Extension class
private Map<?,?> envVars;
Gradle tells me that it can not set the property settings. So what I would like to achieve is to set a map of values in my extension class.
What I did achieve is to get the closure when i write the following:
settings {
envVars {
a = "abc"
b = "dec"
...
n = "sdf"
}
}
public class extension {
....
public envVars(Closure c){}
}
But then I have no clue what to do with the closure and how to access what is inside, so I would rather have a Map instead of the closure
Regards
Mathias
Ok, you just have to write the map properly :/
envVars = [test: 'test']
and everything is fine
I am using the following to read a map of values from build.gradle
reference: https://github.com/liquibase/liquibase-gradle-plugin
Container class:
class Database {
def name
def arguments = [logLevel: 'info']
Database(String name) {
this.name = name
}
Extension Class:
class MyExtension {
final NamedDomainObjectContainer<Database> databases
def databases(Closure closure){
databases.configure(closure)
}
def methodMissing(String name, args) {
arguments[name] = args[0]
}
}
Load Extensions
def databases = project.container(Database) { name ->
new Database(name)
}
project.configure(project) {
extensions.create("extensionName", MyExtension, databases)
}
Sample build.gradle:
dbDiff{
databases{
db1{
url 'testUrl'
}
}
}

Interoperation between Java library Args4j and Scala 2.10

I'd like to use the args4j Java library in my Scala v2.10 application. However, I am running into some trouble. As an example, I have a class to store my arguments in Java as follows.
public class MyArgs {
#Option(name = "-i", usage = "file input", required = true)
private String input;
public String getInput() { return input; }
}
I then have a Scala program like the following.
object TestArgs {
def main(args: Array[String]): Unit = {
val myArgs = new MyArgs()
val cmdLineParser = new CmdLineParser()
try {
cmdLineParser.parseArgument(java.util.Arrays.asList(args: _*))
} catch {
case e: Exception => {
System.err.println(e.getMessage)
}
}
}
}
In IntelliJ, I pass in something like -i some/path/to/file.txt, but i keep getting the following message.
"-i" is not a valid option
Any ideas on what's going on?
You can use the args4j Java library directly; however, might I suggest this wrapper library.
Scala helper using args4j
This implementation works for me using Scala 2.10, Spark 1.6
// import the Java args4j
import org.kohsuke.args4j.Option
// import the scala wrapper
import OptionsHelper.optionsOrExit
object SparkScalaTest {
def main(args: scala.Array[String]) {
// setupt the options
class MyOptions extends OptionsWithHelp {
#Option(name="--foo", aliases=Array("-f"), usage="foo bar", required=false)
var update: Boolean = false // this is the default value, since it is not required
}
//Parse arguments
val options = optionsOrExit(args, new MyOptions)
sc.stop
}
}

Categories