/*
* DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2013-2014 ForgeRock AS. All rights reserved.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openicf.misc.scriptedcommon;
import static org.forgerock.openicf.misc.scriptedcommon.ScriptedConnectorBase.CONFIGURATION;
import static org.forgerock.openicf.misc.scriptedcommon.ScriptedConnectorBase.LOGGER;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.common.security.SecurityUtil;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.spi.AbstractConfiguration;
import org.identityconnectors.framework.spi.ConfigurationClass;
import org.identityconnectors.framework.spi.ConfigurationProperty;
import org.identityconnectors.framework.spi.StatefulConfiguration;
import org.identityconnectors.framework.spi.operations.AuthenticateOp;
import org.identityconnectors.framework.spi.operations.CreateOp;
import org.identityconnectors.framework.spi.operations.DeleteOp;
import org.identityconnectors.framework.spi.operations.ResolveUsernameOp;
import org.identityconnectors.framework.spi.operations.SchemaOp;
import org.identityconnectors.framework.spi.operations.ScriptOnResourceOp;
import org.identityconnectors.framework.spi.operations.SearchOp;
import org.identityconnectors.framework.spi.operations.SyncOp;
import org.identityconnectors.framework.spi.operations.TestOp;
import org.identityconnectors.framework.spi.operations.UpdateOp;
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Script;
import groovy.util.ConfigObject;
import groovy.util.ConfigSlurper;
import groovy.util.DelegatingScript;
import groovy.util.GroovyScriptEngine;
import groovy.util.ResourceException;
import groovy.util.ScriptException;
/**
* Extends the {@link AbstractConfiguration} class to provide all the necessary
* parameters to initialize the Scripted Connector.
*
* @author Gael Allioux <gael.allioux@forgerock.com>
*/
@ConfigurationClass(skipUnsupported = true)
public class ScriptedConfiguration extends AbstractConfiguration implements StatefulConfiguration {
/**
* Setup logging for the {@link ScriptedConnectorBase}.
*/
private static final Log logger = Log.getLog(ScriptedConfiguration.class);
private static final String DOT_STAR = ".*";
private static final String EMPTY_STRING = "";
private final CompilerConfiguration config;
{
config = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
config.setSourceEncoding("UTF-8");
}
// =======================================================================
// Operation Script Files
// =======================================================================
/**
* Authenticate script filename
*/
private String authenticateScriptFileName = null;
/**
* Return the Authenticate script FileName
*
* @return value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = AuthenticateOp.class)
public String getAuthenticateScriptFileName() {
return authenticateScriptFileName;
}
/**
* Set the Authenticate script FileName
*
* @param value
*/
public void setAuthenticateScriptFileName(String value) {
this.authenticateScriptFileName = value;
}
/**
* Create script filename
*/
private String createScriptFileName = null;
/**
* Return the Create script FileName
*
* @return value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = CreateOp.class)
public String getCreateScriptFileName() {
return createScriptFileName;
}
/**
* Set the Create script FileName
*
* @param value
*/
public void setCreateScriptFileName(String value) {
this.createScriptFileName = value;
}
/**
* Update script FileName
*/
private String updateScriptFileName = null;
/**
* Return the Update script FileName
*
* @return updateScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = UpdateOp.class)
public String getUpdateScriptFileName() {
return updateScriptFileName;
}
/**
* Set the Update script FileName
*
* @param value
*/
public void setUpdateScriptFileName(String value) {
this.updateScriptFileName = value;
}
/**
* Delete script FileName
*/
private String deleteScriptFileName = null;
/**
* Return the Delete script FileName
*
* @return deleteScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = DeleteOp.class)
public String getDeleteScriptFileName() {
return deleteScriptFileName;
}
/**
* Set the Delete script FileName
*
* @param value
*/
public void setDeleteScriptFileName(String value) {
this.deleteScriptFileName = value;
}
/**
* Test script FileName
*/
private String resolveUsernameScriptFileName = null;
/**
* Return the Test script FileName
*
* @return resolveUsernameScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = ResolveUsernameOp.class)
public String getResolveUsernameScriptFileName() {
return resolveUsernameScriptFileName;
}
/**
* Set the Test script FileName
*
* @param value
*/
public void setResolveUsernameScriptFileName(String value) {
this.resolveUsernameScriptFileName = value;
}
/**
* ScriptOnResource script FileName
*/
private String scriptOnResourceScriptFileName = null;
/**
* Return the ScriptOnResource script FileName
*
* @return scriptOnResourceScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = ScriptOnResourceOp.class)
public String getScriptOnResourceScriptFileName() {
return scriptOnResourceScriptFileName;
}
/**
* Set the ScriptOnResource script FileName
*
* @param value
*/
public void setScriptOnResourceScriptFileName(String value) {
this.scriptOnResourceScriptFileName = value;
}
/**
* Search script FileName
*/
private String searchScriptFileName = null;
/**
* Return the Search script FileName
*
* @return searchScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = SearchOp.class)
public String getSearchScriptFileName() {
return searchScriptFileName;
}
/**
* Set the Search script FileName
*
* @param value
*/
public void setSearchScriptFileName(String value) {
this.searchScriptFileName = value;
}
/**
* Sync script FileName
*/
private String syncScriptFileName = null;
/**
* Return the Sync script FileName
*
* @return syncScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts", operations = SyncOp.class)
public String getSyncScriptFileName() {
return syncScriptFileName;
}
/**
* Set the Sync script FileName
*
* @param value
*/
public void setSyncScriptFileName(String value) {
this.syncScriptFileName = value;
}
/**
* Schema script FileName
*/
private String schemaScriptFileName = null;
/**
* Return the Schema script FileName
*
* @return schemaScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts",
operations = SchemaOp.class)
public String getSchemaScriptFileName() {
return schemaScriptFileName;
}
/**
* Set the Schema script FileName
*
* @param value
*/
public void setSchemaScriptFileName(String value) {
this.schemaScriptFileName = value;
}
/**
* Test script FileName
*/
private String testScriptFileName = null;
/**
* Return the Test script FileName
*
* @return testScriptFileName value
*/
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts", operations = TestOp.class)
public String getTestScriptFileName() {
return testScriptFileName;
}
/**
* Set the Test script FileName
*
* @param value
*/
public void setTestScriptFileName(String value) {
this.testScriptFileName = value;
}
// =======================================================================
// Groovy Engine configuration
// =======================================================================
/**
* Gets extensions used to find a groovy files
*
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public String[] getScriptExtensions() {
return config.getScriptExtensions()
.toArray(new String[config.getScriptExtensions().size()]);
}
public void setScriptExtensions(String[] scriptExtensions) {
config.setScriptExtensions(null != scriptExtensions ? new HashSet<String>(Arrays
.asList(scriptExtensions)) : null);
}
/**
* Gets the currently configured warning level. See WarningMessage for level
* details.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public int getWarningLevel() {
return config.getWarningLevel();
}
/**
* Sets the warning level. See WarningMessage for level details.
*/
public void setWarningLevel(int level) {
config.setWarningLevel(level);
}
/**
* Gets the currently configured source file encoding.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public String getSourceEncoding() {
return config.getSourceEncoding();
}
/**
* Sets the encoding to be used when reading source files.
*/
public void setSourceEncoding(String encoding) {
config.setSourceEncoding(encoding);
}
/**
* Gets the target directory for writing classes.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public File getTargetDirectory() {
return config.getTargetDirectory();
}
/**
* Sets the target directory.
*/
public void setTargetDirectory(File directory) {
this.config.setTargetDirectory(directory);
}
/**
* @return the classpath
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine", required = true)
public String[] getClasspath() {
if (null != config.getClasspath()) {
return config.getClasspath().toArray(new String[config.getClasspath().size()]);
}
return null;
}
/**
* Sets the classpath.
*/
public void setClasspath(String[] classpath) {
config.setClasspathList(Arrays.asList(classpath));
}
private String[] scriptRoots = null;
/**
* @return the script roots
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine", required = true)
public String[] getScriptRoots() {
return scriptRoots;
}
/**
* Sets the script root folders.
*/
public void setScriptRoots(String[] scriptRoots) {
this.scriptRoots = scriptRoots;
}
/**
* Returns true if verbose operation has been requested.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public boolean getVerbose() {
return config.getVerbose();
}
/**
* Turns verbose operation on or off.
*/
public void setVerbose(boolean verbose) {
config.setVerbose(verbose);
}
/**
* Returns true if debugging operation has been requested.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public boolean getDebug() {
return config.getDebug();
}
/**
* Turns debugging operation on or off.
*/
public void setDebug(boolean debug) {
config.setDebug(debug);
}
/**
* Returns the requested error tolerance.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public int getTolerance() {
return config.getTolerance();
}
/**
* Sets the error tolerance, which is the number of non-fatal errors (per
* unit) that should be tolerated before compilation is aborted.
*/
public void setTolerance(int tolerance) {
config.setTolerance(tolerance);
}
/**
* Gets the name of the base class for scripts. It must be a subclass of
* Script.
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public String getScriptBaseClass() {
return config.getScriptBaseClass();
}
/**
* Sets the name of the base class for scripts. It must be a subclass of
* Script.
*/
public void setScriptBaseClass(String scriptBaseClass) {
config.setScriptBaseClass(scriptBaseClass);
}
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public boolean getRecompileGroovySource() {
return config.getRecompileGroovySource();
}
public void setRecompileGroovySource(boolean recompile) {
config.setRecompileGroovySource(recompile);
}
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public int getMinimumRecompilationInterval() {
return config.getMinimumRecompilationInterval();
}
public void setMinimumRecompilationInterval(int time) {
config.setMinimumRecompilationInterval(time);
}
/**
* Returns the list of disabled global AST transformation class names.
*
* @return a list of global AST transformation fully qualified class names
*/
@ConfigurationProperty(groupMessageKey = "groovy.engine")
public String[] getDisabledGlobalASTTransformations() {
if (null != config.getDisabledGlobalASTTransformations()) {
return config.getDisabledGlobalASTTransformations().toArray(new String[0]);
}
return null;
}
/**
* {@link CompilerConfiguration#setDisabledGlobalASTTransformations(java.util.Set)}
*/
public void setDisabledGlobalASTTransformations(final String[] disabledGlobalASTTransformations) {
if (null != disabledGlobalASTTransformations) {
config.setDisabledGlobalASTTransformations(new HashSet<String>(Arrays
.asList(disabledGlobalASTTransformations)));
} else {
config.setDisabledGlobalASTTransformations(null);
}
}
// =======================================================================
// Other Configuration Properties
// =======================================================================
@ConfigurationProperty(groupMessageKey = "groovy.operation.scripts")
public String getCustomizerScriptFileName() {
return customizerScriptFileName;
}
public void setCustomizerScriptFileName(String customizerScriptFileName) {
this.customizerScriptFileName = customizerScriptFileName;
}
private String customizerScriptFileName = null;
@ConfigurationProperty
public String getCustomConfiguration() {
return customConfiguration;
}
public void setCustomConfiguration(String customConfiguration) {
this.customConfiguration = customConfiguration;
if (StringUtil.isNotBlank(this.customConfiguration)) {
ConfigObject config = new ConfigSlurper().parse(this.customConfiguration);
mergeConfig(propertyBag, config, false);
}
}
private String customConfiguration = null;
@ConfigurationProperty(confidential = true)
public GuardedString getCustomSensitiveConfiguration() {
return customSensitiveConfiguration;
}
public void setCustomSensitiveConfiguration(GuardedString customConfiguration) {
this.customSensitiveConfiguration = customConfiguration;
if (null != this.customSensitiveConfiguration) {
ConfigObject config =
new ConfigSlurper().parse(SecurityUtil
.decrypt(this.customSensitiveConfiguration));
mergeConfig(propertyBag, config, true);
}
}
private GuardedString customSensitiveConfiguration = null;
private void mergeConfig(final Map config, final Map other, boolean overwrite) {
for (Object o : other.entrySet()) {
final Object key = ((Map.Entry) o).getKey();
final Object value = ((Map.Entry) o).getValue();
final Object entry = config.get(key);
if (entry instanceof Map && ((Map) entry).size() > 0 && value instanceof Map) {
mergeConfig((Map) entry, (Map) value, overwrite);
} else if (entry == null || overwrite) {
config.put(key, value);
}
}
}
// =======================================================================
// Methods for Script writers
// =======================================================================
private final ConcurrentMap<String, Object> propertyBag =
new ConcurrentHashMap<String, Object>();
private Closure releaseClosure = null;
public void setReleaseClosure(Closure releaseClosure) {
this.releaseClosure = releaseClosure;
}
public Closure getReleaseClosure() {
return releaseClosure;
}
/**
* Returns the Map shared between the Connector instances.
*
* Shared map to store initialised resources which should be shared between
* the scripts.
*
* @return single instance of shared ConcurrentMap.
*/
public ConcurrentMap<String, Object> getPropertyBag() {
return propertyBag;
}
// =======================================================================
// Interface Implementation
// =======================================================================
public void release() {
synchronized (this) {
Closure c = getReleaseClosure();
if (null != c) {
Closure clone = c.rehydrate(this, this, this);
clone.setResolveStrategy(Closure.DELEGATE_FIRST);
clone.call();
releaseClosure = null;
}
groovyScriptEngine = null;
propertyBag.clear();
loggerCache.clear();
logger.ok("Shared state ScriptedConfiguration is successfully released");
}
}
/**
* {@inheritDoc}
*/
public void validate() {
logger.info("Load and compile configured scripts");
if (getClasspath() == null || getClasspath().length < 1) {
throw new ConfigurationException("Missing required 'classpath' configuration property");
}
validateScript(getAuthenticateScriptFileName());
validateScript(getCreateScriptFileName());
validateScript(getDeleteScriptFileName());
validateScript(getResolveUsernameScriptFileName());
validateScript(getSchemaScriptFileName());
validateScript(getScriptOnResourceScriptFileName());
validateScript(getSearchScriptFileName());
validateScript(getSyncScriptFileName());
validateScript(getTestScriptFileName());
validateScript(getUpdateScriptFileName());
logger.info("Load and compile of scripts are successful");
}
protected void validateScript(String scriptName) {
try {
loadScript(scriptName);
} catch (Throwable t) {
logger.error(t, "Failed to validate and load script: {0}", scriptName);
throw ConnectorException.wrap(t);
}
}
// =======================================================================
//
// =======================================================================
protected String getDefaultCustomizerScriptName() {
return "/" + getClass().getPackage().getName().replace('.', '/') + "/CustomizerScript.groovy";
}
protected Class getCustomizerClass() {
Class customizerClass = null;
if (StringUtil.isBlank(customizerScriptFileName)) {
URL url = getClass().getResource(getDefaultCustomizerScriptName());
if (null != url) {
GroovyCodeSource source = null;
try {
source =
new GroovyCodeSource(ResourceGroovyMethods.getText(url,
getSourceEncoding()), url.toExternalForm(), "/groovy/script");
} catch (IOException e) {
throw ConnectorException.wrap(e);
}
source.setCachable(false);
customizerClass = getGroovyScriptEngine().getGroovyClassLoader().parseClass(source);
}
} else {
customizerClass = loadScript(customizerScriptFileName);
}
return customizerClass;
}
protected Script createCustomizerScript(Class customizerClass, Binding binding) {
binding.setVariable(CONFIGURATION, this);
return InvokerHelper.createScript(customizerClass, binding);
}
private final ConcurrentMap<String, Log> loggerCache = new ConcurrentHashMap<String, Log>(11);
Object evaluate(String scriptName, Binding binding, Object delegate) throws Exception {
try {
Script scr = getGroovyScriptEngine().createScript(scriptName, binding);
binding.setVariable(LOGGER, getLogger(scr.getClass()));
if (scr instanceof DelegatingScript && null != delegate) {
((DelegatingScript) scr).setDelegate(delegate);
}
return scr.run();
} catch (ResourceException e) {
throw ConnectorException.wrap(e);
} catch (ScriptException e) {
throw ConnectorException.wrap(e);
}
}
Class loadScript(String scriptName) {
if (StringUtil.isNotBlank(scriptName)) {
try {
return getGroovyScriptEngine().loadScriptByName(scriptName);
} catch (ResourceException e) {
throw ConnectorException.wrap(e);
} catch (ScriptException e) {
throw ConnectorException.wrap(e);
}
}
return null;
}
protected Log getLogger(final Class<?> clazz) {
final String key = clazz.getName();
Log logger = loggerCache.get(key);
if (null == logger) {
logger = Log.getLog(clazz);
Log l = loggerCache.putIfAbsent(key, logger);
if (l != null) {
logger = l;
}
}
return logger;
}
private GroovyScriptEngine groovyScriptEngine = null;
protected GroovyScriptEngine getGroovyScriptEngine() {
if (null == groovyScriptEngine) {
synchronized (this) {
if (null == groovyScriptEngine) {
final CompilerConfiguration compilerConfiguration =
new CompilerConfiguration(config);
compilerConfiguration.addCompilationCustomizers(getImportCustomizer(null));
final GroovyClassLoader loader =
new GroovyClassLoader(getParentLoader(), compilerConfiguration, true);
groovyScriptEngine =
new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);
initializeCustomizer();
}
}
}
return groovyScriptEngine;
}
/*
* This must be called once from thread-safe location and inside the
* synchronized to avoid deadlock.
*/
private void initializeCustomizer() {
try {
Class customizerClass = getCustomizerClass();
if (null != customizerClass) {
Binding binding = new Binding();
binding.setVariable(LOGGER, getLogger(customizerClass));
createCustomizerScript(customizerClass, binding).run();
}
} catch (Throwable t) {
logger.error(t, "Failed to customize the connector");
throw ConnectorException.wrap(t);
}
}
protected ClassLoader getParentLoader() {
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
try {
Class c = contextLoader.loadClass(Script.class.getName());
if (c == Script.class) {
return contextLoader;
}
} catch (ClassNotFoundException e) {
/* ignore */
}
return Script.class.getClassLoader();
}
protected URL[] getRoots(CompilerConfiguration compilerConfiguration, GroovyClassLoader loader) {
// Do not allow this because it collides with the classes from
// parent. For safety remove this from roots
URL forbiddenLocation = getClass().getProtectionDomain().getCodeSource().getLocation();
List<URL> safeRoots = new ArrayList<URL>();
String[] customRoots = getScriptRoots();
if (null != customRoots && customRoots.length > 0) {
for (String sr : customRoots) {
if (null != sr) {
try {
URL root = new File(sr).toURI().toURL();
if (forbiddenLocation.equals(root)) {
logger.info(
"The connector source location is removed from the roots. This url is not allowed: {0}",
forbiddenLocation);
}
safeRoots.add(root);
} catch (MalformedURLException e) {
throw new ConfigurationException(e.getMessage(), e);
}
}
}
} else {
logger.ok("Fallback to use the classpath for scripts.");
}
if (safeRoots.isEmpty()) {
for (URL root : loader.getURLs()) {
if (forbiddenLocation.equals(root)) {
logger.info(
"The connector source location is removed from the roots. This url is not allowed: {0}",
forbiddenLocation);
}
safeRoots.add(root);
}
}
return safeRoots.toArray(new URL[safeRoots.size()]);
}
protected ImportCustomizer getImportCustomizer(ImportCustomizer parent) {
final ImportCustomizer ic = null != parent ? parent : new ImportCustomizer();
for (final String imp : getImports()) {
ic.addStarImports(imp.replace(DOT_STAR, EMPTY_STRING));
}
ic.addImport("ICF", ICFObjectBuilder.class.getName());
return ic;
}
protected String[] getImports() {
return new String[] { ConnectorObject.class.getPackage().getName() + ".*" };
}
}