package eu.esdihumboldt.util.groovy.sandbox; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.Platform; import org.kohsuke.groovy.sandbox.SandboxTransformer; import de.fhg.igd.eclipse.util.extension.ExtensionUtil; import eu.esdihumboldt.util.groovy.sandbox.internal.RestrictiveGroovyInterceptor; import eu.esdihumboldt.util.groovy.sandbox.internal.RestrictiveGroovyInterceptor.AllowedPrefix; import eu.esdihumboldt.util.groovy.sandbox.internal.SecureScript; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; /** * Default implementation of the {@link GroovyService} interface. * * @author Kai Schwierczek */ public class DefaultGroovyService implements GroovyService { /** * Extension point ID. */ private static final String ID = "eu.esdihumboldt.util.groovy.sandbox"; private final CopyOnWriteArraySet<GroovyServiceListener> listeners = new CopyOnWriteArraySet<GroovyServiceListener>(); private boolean restrictionActive = true; private final RestrictiveGroovyInterceptor interceptor; /** * Constructs a new groovy sandbox service. Use * eu.esdihumboldt.util.groovy.sandbox extension point for configuration * options. */ public DefaultGroovyService() { interceptor = createInterceptorFromExtensions(); } /** * @return the Groovy interceptor configured with the allowed classes from * the extension point. */ public static RestrictiveGroovyInterceptor createInterceptorFromExtensions() { Set<Class<?>> additionalAllowedClasses = new HashSet<>(); Set<Class<?>> additionalAllAllowedClasses = new HashSet<>(); List<AllowedPrefix> additionalAllowedPackages = new ArrayList<>(); for (IConfigurationElement conf : Platform.getExtensionRegistry() .getConfigurationElementsFor(ID)) { if (conf.getName().equals("allow")) { boolean allowAll = Boolean.parseBoolean(conf.getAttribute("allowAll")); Class<?> allowedClass = ExtensionUtil.loadClass(conf, "class"); if (allowAll) additionalAllAllowedClasses.add(allowedClass); else additionalAllowedClasses.add(allowedClass); } if (conf.getName().equals("allowPackage")) { boolean allowChildren = Boolean.parseBoolean(conf.getAttribute("allowChildren")); String packageName = conf.getAttribute("name"); additionalAllowedPackages.add(new AllowedPrefix(packageName, allowChildren)); } } return new RestrictiveGroovyInterceptor(additionalAllowedClasses, additionalAllAllowedClasses, additionalAllowedPackages); } @Override public GroovyShell createShell(Binding binding) { // TODO use a specific classloader? CompilerConfiguration cc = new CompilerConfiguration(); // add pre-defined imports ImportCustomizer importCustomizer = new ImportCustomizer(); // add extension-defined imports configureImportsFromExtensions(importCustomizer); cc.addCompilationCustomizers(importCustomizer); if (isRestrictionActive()) { // configure restriction cc.addCompilationCustomizers(new SandboxTransformer()); cc.setScriptBaseClass(SecureScript.class.getName()); } if (binding != null) return new GroovyShell(binding, cc); else return new GroovyShell(cc); } /** * Add imports defined as extensions. * * @param importCustomizer the import customizer */ private static void configureImportsFromExtensions(ImportCustomizer importCustomizer) { for (IConfigurationElement conf : Platform.getExtensionRegistry() .getConfigurationElementsFor(ID)) { if (conf.getName().equals("import")) { String className = conf.getAttribute("class"); String alias = conf.getAttribute("alias"); if (className != null && !className.isEmpty()) { if (alias == null || alias.isEmpty()) { int lastDotIndex = className.lastIndexOf('.'); if (lastDotIndex >= 0) { if (lastDotIndex < className.length() - 1) { alias = className.substring(lastDotIndex + 1); } else { alias = null; } } else { alias = className; } } if (alias != null) { importCustomizer.addImport(alias, className); } } } // TODO support also other kind of imports? // e.g. star imports, static imports... } } @Override public Script parseScript(String script, Binding binding) { return createShell(binding).parse(script); } @Override public <T> T evaluate(Script script, @Nullable ResultProcessor<T> processor) throws Exception { boolean registered = false; if (isRestrictionActive()) { if (!(script instanceof SecureScript)) { throw new GroovyRestrictionException( "Supplied script was not parsed with active restriction."); } interceptor.register(); registered = true; } try { Object returnValue = script.run(); if (processor != null) { return processor.process(script, returnValue); } else { return null; } } finally { if (registered) interceptor.unregister(); } } @Override public boolean isRestrictionActive() { return restrictionActive; } @Override public void setRestrictionActive(boolean active) { if (active != restrictionActive) { restrictionActive = active; notifyRestrictionChanged(active); } } @Override public void addListener(GroovyServiceListener listener) { listeners.add(listener); } @Override public void removeListener(GroovyServiceListener listener) { listeners.remove(listener); } /** * Call when restriction active changes. * * @param restrictionActive the new value */ protected void notifyRestrictionChanged(boolean restrictionActive) { for (GroovyServiceListener listener : listeners) { listener.restrictionChanged(restrictionActive); } } }