Executing named scripts as System in Alfresco

Sometimes you are assigned to a running project, and wonder why some design decisions have been made. I am facing rule-triggered scripts trying to change permissions on a folder structure, that travels the repository (folder based routing alike). And in this project there is (of course) no time/budget to decently solve this. This results in a system where no one should change any permission because it results in a failing system. (The folder structure has nodes with -very- limited permissions for most users, therefore coordinators cannot modify them). These scripts need to run as System of course. There are quite a bunch of these, and a simple, quick solution has my preference (it’s my own time right?).

This blog describes how I tweaked the class responsible for executing scripts in Alfresco. A set of scripts can be named that will be executed as System user, therefore eliminating my issues with permissions. All other scripts will be run as before, having the context of the user invoking them. It is a follow up on my ‘Sudo for Scripts in Alfresco‘ post, originated from the discussion with Fabio Strozzi.

We change a class/bean, and therefore need to modify the bean configuration as well. The class RhinoScriptProcessor can be found in the Alfresco public SVN. Put it in the proper place of your Eclipse project, as SudoRhinoScriptProcessor.java. Since I added a list of named scripts that are allowed to run as System (in the bean definition), we need to read them into a List of Strings (define this List<String> allowedScripts as well of course).

    /**
     * @param allowedScripts	The list of scripts to be executed as System instead of user
     */
    public void setAllowedScripts(List&amp;lt;String&amp;gt; allowedScripts)
    {
    	// @ToDo Convert list of readable Alfresco repo path's into noderefs,
    	// and/or check if classpat: path's are used
        this.allowedScripts = allowedScripts;
    }

The actual magic happens in the method executeScriptImpl, in the bottom part of the code.

In the first few lines we resolve the noderef of the Script object at hand into the script name. (Can be done more efficient?)

String modelscript = model.get("script").toString();
logger.debug("Found script noderef: "+ modelscript);
String name = this.services.getNodeService().getPath(new NodeRef(modelscript)).toString();
name = name.substring(name.lastIndexOf("}")+1,name.length());
name = name.replaceAll("_x0020_", " ");
logger.debug("Transformed to Name: " + name);</pre>
In the end we check if the script at hand is mentioned in the list of script names that should be executed as System. Either execute as before, or use the runAs construct and execute as System.
<pre>    if (allowedScripts.contains(name)){
    	logger.debug("Running " + modelscript + " as System");
    	result = AuthenticationUtil.runAs(raw, AuthenticationUtil.getSystemUserName());
	}
    else
	{
            // execute the script and return the result
    	logger.debug("Running " + modelscript);
            result = script.exec(cx, scope);
	}

The first if statement determines if the name of the script currently to be executed appears in the list of script names defined in the bean definition. If so, run as System. If not, jut run as before…

The entire method looks like this:

private Object executeScriptImpl(final Script script, Map&lt;String, Object&gt; model, boolean secure)
throws AlfrescoRuntimeException
{
String modelscript = model.get("script").toString();
logger.debug("Found script noderef: "+ modelscript);
String name = this.services.getNodeService().getPath(new NodeRef(modelscript)).toString();
name = name.substring(name.lastIndexOf("}")+1,name.length());
name = name.replaceAll("_x0020_", " ");
logger.debug("Transformed to Name: " + name);

long startTime = 0;
if (logger.isDebugEnabled())
{
    startTime = System.nanoTime();
}

// Convert the model
model = convertToRhinoModel(model);

Context cx = Context.enter();
try
{
    // Create a thread-specific scope from one of the shared scopes.
    // See http://www.mozilla.org/rhino/scopes.html
    cx.setWrapFactory(wrapFactory);
    Scriptable scope;
    if (this.shareSealedScopes)
    {
        Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
        scope = cx.newObject(sharedScope);
        scope.setPrototype(sharedScope);
        scope.setParentScope(null);
    }
    else
    {
        scope = initScope(cx, secure, false);
    }

    // there's always a model, if only to hold the util objects
    if (model == null)
    {
        model = new HashMap&lt;String, Object&gt;();
    }

    // add the global scripts
    for (ProcessorExtension ex : this.processorExtensions.values())
    {
        model.put(ex.getExtensionName(), ex);
    }

    // insert supplied object model into root of the default scope
    for (String key : model.keySet())
    {
        // set the root scope on appropriate objects
        // this is used to allow native JS object creation etc.
        Object obj = model.get(key);
        if (obj instanceof Scopeable)
        {
            ((Scopeable)obj).setScope(scope);
        }

        // convert/wrap each object to JavaScript compatible
        Object jsObject = Context.javaToJS(obj, scope);

        // insert into the root scope ready for access by the script
        ScriptableObject.putProperty(scope, key, jsObject);
    }

    //does this actually work???
    final Context cxInternal = cx;
    final Scriptable scopeInternal = scope;
    // innerclass to run as System if needed
    RunAsWork&lt;Object&gt; raw = new RunAsWork&lt;Object&gt;() {
        public Object doWork() throws Exception {
        	return  script.exec(cxInternal, scopeInternal);
        }
    };

    Object result = null;
    if (allowedScripts.contains(name)){
    	logger.debug("Running " + modelscript + " as System");
    	result = AuthenticationUtil.runAs(raw, AuthenticationUtil.getSystemUserName());
	}
    else
	{
            // execute the script and return the result
    	logger.debug("Running " + modelscript);
            result = script.exec(cx, scope);
	}

    // extract java object result if wrapped by Rhino
    return (Object) valueConverter.convertValueForJava(result);
}
catch (WrappedException w)
{
    Throwable err = w.getWrappedException();
    if (err instanceof RuntimeException)
    {
        throw (RuntimeException)err;
    }
    throw new AlfrescoRuntimeException(err.getMessage(), err);
}
catch (Throwable err)
{
    throw new AlfrescoRuntimeException(err.getMessage(), err);
}
finally
{
    Context.exit();

    if (logger.isDebugEnabled())
    {
        long endTime = System.nanoTime();
        logger.debug("Time to execute script: " + (endTime - startTime)/1000000f + "ms");
    }
}
}

That should do for the Java code. Remind, I tested with scripts from the DataDictionary only (since all our scripts for this project live here). Enhancements for classpath-scripts can be customized in later… (Suggestions??) Compile the code, and put it in your classpath (e.g. create a jar, or put the class file structure in your tomcat/shared/classes/alfresco/extensions)

In order to actually load this class and replace the original one, we need a bit of XML. Add or edit the bean definition tomcat/shared/classes/alfresco/extensions/script-services-context.xml:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN'
'http://www.springframework.org/dtd/spring-beans.dtd'>

<beans>

    <bean id="javaScriptProcessor" class="org.alfresco.repo.jscript.SudoRhinoScriptProcessor" init-method="register">
       <property name="name">
            <value>javascript</value>
        </property>
        <property name="extension">
            <value>js</value>
        </property>
        <!-- compile javascript and cache compiled scripts -->
        <property name="compile">
            <value>true</value>
        </property>
        <!-- allow sharing of sealed scopes for performance -->
        <!-- disable to give each script it's own new scope which can be extended -->
        <property name="shareSealedScopes">
            <value>true</value>
        </property>
        <property name="scriptService">
            <ref bean="scriptService"/>
        </property>
        <!-- Creates ScriptNodes which require the ServiceRegistry -->
        <property name="serviceRegistry">
            <ref bean="ServiceRegistry"/>
        </property>
        <property name="storeUrl">
            <value>${spaces.store}</value>
        </property>
        <property name="storePath">
            <value>${spaces.company_home.childname}</value>
        </property>

        <property name="allowedScripts">
          <list>
            <value>alfresco docs.js</value>
          </list>
        </property>
    </bean>
</beans>

The property ‘allowedScripts’  lists a list of scripts that are to be run as System account instead of the user invoking the script.

Restart your system, and let the fun begin. I work with a config where nobody has write access to CompanyHome, and users see only one of the many folders inside ComapnyHome. I modified the search in ‘DataDictionary/scripts/alfresco docs.js’ into a meaningfull search term. I executed the script against the CompanyHome space. The result:

  • Any ordinary user with very limited permissions can find all documents with the relevant search term
  • Any ordinary user can write the result in CompanyHome where only admin has write permissions, and all other ones are Consumer only.

Voila!

Advertisements

3 Responses to “Executing named scripts as System in Alfresco”


  1. 1 nassimaimeur@yahoo.fr May 20, 2012 at 18:48

    can you help me where i put the java file and xml file i want execute the javascript with rule and admin privilege


  1. 1 Invoking scripts in Alfresco programatically | Václav Balák Trackback on March 28, 2012 at 17:23
Comments are currently closed.