Q:

Groovy script sandboxing: Use Groovy TimeCategory syntax from Java as String

I have Java application with API. API allows using flexible date syntax:
PUT /monthly-jobs/job1 
{
    "startExpression": "now + 1.week + 4.days + 2.hours"
}

For this task I want to utilize Groovy's TimeCategory class (http://groovycookbook.org/basic_types/dates_times/).
In my opinion, there should be a helper Java class for this according to interface:
interface DateExpressionEvaluator {
    Date evaluateDateExpression(String expr);
}

What's is a correct approach for this? How to use Groovy in such requirements?
Please note I cannot evaluate input as groovy code plainly, as API user may insert groovy code and hack the server.

A:

I had to create a solution using groovy script sandboxing (https://github.com/kohsuke/groovy-sandbox):
Test case:
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.util.Date;

import org.apache.commons.lang3.time.FastDateFormat;
import org.junit.Test;

public class DateExpressionUtilsTest {

    @Test
    public void test_parse() {
        assertEquals(
                FastDateFormat.getInstance("yyyy-MM-dd hh:mm").format(
                        new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)),
                FastDateFormat.getInstance("yyyy-MM-dd hh:mm").format(DateExpressionUtils.eval("now + 1.week")));

        try {
            DateExpressionUtils.eval("now + 1.week; Thread.sleep(1000);");
            fail();
        } catch (SecurityException e) {
            // ok
        }
    }
}

DateExpressionUtils:
import groovy.lang.Binding;
import groovy.lang.GroovyShell;

import java.util.Date;

import org.codehaus.groovy.control.CompilerConfiguration;
import org.kohsuke.groovy.sandbox.SandboxTransformer;

/**
 * Sandboxing: https://github.com/kohsuke/groovy-sandbox
 * http://groovy-sandbox.kohsuke.org/
 *
 */

public class DateExpressionUtils {
    public static Date eval(String expr) {
        try {
            CompilerConfiguration cc = new CompilerConfiguration();
            cc.addCompilationCustomizers(new SandboxTransformer());
            Binding binding = new Binding();
            binding.setProperty("now", new Date());
            GroovyShell sh = new GroovyShell(binding, cc);
            DateExpressionSandbox sandbox = new DateExpressionSandbox();
            sandbox.register();
            try {
                Object resObj = sh.evaluate("use(groovy.time.TimeCategory){" + expr + "}");
                Date res = (Date) resObj;
                return res;
            } finally {
                sandbox.unregister();
            }
        } catch (SecurityException e) {
            throw new SecurityException(String.format("Possible date expression sandbox jailbreak with '%s': '%s'.",
                    expr, e.getMessage()));
        } catch (Exception e) {
            throw new RuntimeException(String.format("Unable to evaluate date expression '%s': '%s'.", expr,
                    e.getMessage()));
        }
    }
}

DateExpressionSandbox:
import groovy.lang.Closure;
import groovy.lang.Script;
import groovy.time.Duration;
import groovy.time.TimeCategory;

import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.kohsuke.groovy.sandbox.GroovyValueFilter;

/**
 * Example sandbox: https://github.com/kohsuke/groovy-sandbox/blob/master/src/test/groovy/org/
 * kohsuke/groovy/sandbox/robot/RobotSandbox.groovy
 *
 */

public class DateExpressionSandbox extends GroovyValueFilter {
    @Override
    public Object filter(Object o) {
        if (o == null || ALLOWED_TYPES.contains(o.getClass()))
            return o;

        if (Class.class.equals(o.getClass()) && ALLOWED_STATIC_CLASSES.contains(o.toString())) {
            return o;
        }

        if (o instanceof Script || o instanceof Closure)
            return o; // access to properties of compiled groovy script

        throw new SecurityException(String.format("Unexpected type: '%s', '%s'.", o.getClass(), o));
    }

    private static final Set<Class<?>> ALLOWED_TYPES = new HashSet<Class<?>>(Arrays.asList(String.class, Integer.class,
            Long.class, Double.class, Boolean.class, Date.class, TimeCategory.class, Duration.class));

    private static final Set<String> ALLOWED_STATIC_CLASSES = new HashSet<>();
    static {
        for (Class<?> cl : ALLOWED_TYPES) {
            ALLOWED_STATIC_CLASSES.add("class " + cl.getCanonicalName());
        }
    }
}

