[ 
https://issues.apache.org/jira/browse/GROOVY-11813?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18048336#comment-18048336
 ] 

Saravanan Subiramaniam commented on GROOVY-11813:
-------------------------------------------------

Debugged and found that using a custom MetaClassImpl is the difference. In our 
project, we want to implement security to allow/dis-allow access to 
methods/constructors/static method calls. For example, we want to throw error 
if java.sql or java.util.concurrent APIs are used in Groovy script. So, we have 
implemented custom SecureMetaClass (which extends MetaClass) and overridden 
methods like {{{}invokeConstructor{}}}, {{invokeStaticMethod}} and 
{{invokeMethod}} to intercept the calls and perform security checks. When I 
remove this SecureMetaClass, then minus operation worked fine in our product. 
For now, we'll use normal MetaClass for Date - to fix this issue. But I'm 
wondering what's wrong with our implementation and is there a recommended/best 
practices for doing such checks. I'm aware of SecureASTCustomizer, but it's a 
compile-time check, which may not cover all the scenarios and hence we wanted 
to do check at runtime.

For reference, this is how we use SecureMetaClass.
{code}
MetaClassRegistry metaClassRegistry = getMetaClassRegistry();
metaClassRegistry.setMetaClassCreationHandle(new 
SecureMetaClassCreationHandle());
{code}

the SecureMetaClassCreationHandle looks like this
{code}
public class SecureMetaClassCreationHandle extends 
MetaClassRegistry.MetaClassCreationHandle {

    @Override
    protected MetaClass createNormalMetaClass(Class theClass, MetaClassRegistry 
registry) {

        if (GeneratedClosure.class.isAssignableFrom(theClass)) {
            return new ClosureMetaClass(registry, theClass);
        } else {
return new SecureMetaClass(registry, theClass);
        }
    }
}
{code}

the SecureMetaClass looks like this
{code}
public class SecureMetaClass extends MetaClassImpl {

    private static final String staticOrNullTarget = "[Static|Null]";
    
    public SecureMetaClass(Class theClass, MetaMethod[] add) {

        super(theClass, add);
    }

    public SecureMetaClass(Class theClass) {

        super(theClass);
    }

    public SecureMetaClass(MetaClassRegistry registry, Class theClass, 
MetaMethod[] add) {

        super(registry, theClass, add);
    }

    public SecureMetaClass(MetaClassRegistry registry, Class theClass) {

        super(registry, theClass);
    }

    @Override
    public CallSite createPojoCallSite(CallSite site, Object receiver, Object[] 
args) {

        CallSiteKey callSiteKey = new CallSiteKey(getReceiverName(receiver), 
site.getName());
        checkRestrictions(receiver, callSiteKey, args);

        return super.createPojoCallSite(site, receiver, args);
    }

    @Override
    public CallSite createStaticSite(CallSite site, Object[] args) {

        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, 
site.getName());
        checkRestrictions(null, callSiteKey, args);

        return super.createStaticSite(site, args);
    }

    @Override
    public Object invokeConstructor(Object[] arguments) {

        MetaReceiver metaReceiver = new MetaReceiver(null, getTheClass());
        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, 
"<$constructor$>");
        checkRestrictions(metaReceiver, callSiteKey, arguments);

        return super.invokeConstructor(arguments);
    }

    @Override
    public Object invokeStaticMethod(Object object, String methodName, Object[] 
arguments) {

        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, 
methodName);
        checkRestrictions(null, callSiteKey, arguments);

        return super.invokeStaticMethod(object, methodName, arguments);
    }

    @Override
    public Object invokeMethod(Object object, String methodName, Object 
arguments) {

        CallSiteKey callSiteKey = new CallSiteKey(getReceiverName(object), 
methodName);

        if (arguments == null) {
            checkRestrictions(object, callSiteKey, MetaClassHelper.EMPTY_ARRAY);
        }
        if (arguments instanceof Tuple<?> tuple) {
            checkRestrictions(object, callSiteKey, tuple.toArray());
        }
        if (arguments instanceof Object[] object1s) {
            checkRestrictions(object, callSiteKey, object1s);
        }

        return super.invokeMethod(object, methodName, arguments);
    }

    private String getReceiverName(Object receiver) {

        if (receiver != null) {
            if (receiver instanceof GroovyObject) {
                return 
GroovyObject.class.cast(receiver).getMetaClass().getTheClass().getCanonicalName();
            } else {
                return receiver.getClass().getCanonicalName();
            }
        } else {
            return staticOrNullTarget;
        }
    }

    private void checkRestrictions(Object object, CallSiteKey callSiteKey, 
Object[] args) {

        // Check if constructor/method/static method calls are allowed based on 
whitelist/blacklist rules
    }

    private static class CallSiteKey {

        private String siteReceiver;
        private String siteName;

        public CallSiteKey(String siteReceiver, String siteName) {

            this.siteReceiver = siteReceiver;
            this.siteName = siteName;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            CallSiteKey that = (CallSiteKey) o;
            return Objects.equals(siteName, that.siteName) &&
                    Objects.equals(siteReceiver, that.siteReceiver);
        }

        @Override
        public int hashCode() {

            return Objects.hash(siteReceiver, siteName);
        }

        @Override
        public String toString() {

            return "CallSiteKey{" +
                    "sitReceiver='" + siteReceiver + "'\''" +
                    ", siteName='" + siteName + '\'' +
                    '}';
        }
    }
}
{code}
 

> groovy.lang.MissingMethodException: No signature of method: minus for class: 
> java.util.Date is applicable for argument types: 
> (groovy.time.DatumDependentDuration)
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-11813
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11813
>             Project: Groovy
>          Issue Type: Question
>    Affects Versions: 5.0.2
>            Reporter: Saravanan Subiramaniam
>            Priority: Major
>
> Could you please help me with TimeCategory? The following script worked fine 
> in Groovy 4.0.x, but fails after we upgraded to 5.0.2.
> {code}
> use(groovy.time.TimeCategory)  { 
>    new Date() - 7.months 
> }
> {code}
> Please note that this script works when running using standalone Groovy (eg: 
> using groovy command), but fails in our application (Java based) with this 
> error. Both groovy-dateutil and groovy-datetime dependencies are in the 
> classpath. Our application uses Java 21.
> {code}
> Dynamic logic TESTUNIT_2:
> 1: *ERROR* -->       use(groovy.time.TimeCategory)  { 
> 2:                    new Date() - 7.months 
> 3:                    }
> groovy.lang.MissingMethodException: No signature of method: minus for class: 
> java.util.Date is applicable for argument types: 
> (groovy.time.DatumDependentDuration) values: [7 months]
> Possible solutions: minus(java.util.Date), minus(int), find(), 
> find(groovy.lang.Closure), is(java.lang.Object), plus(int)
>       at 
> org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:74)
>       at 
> org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.unwrap(IndyGuardsFiltersAndSignatures.java:163)
>       at 
> org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:344)
>       at 
> custom.dynamiclogic.testunit_2.TESTUNIT_2$_run_closure1.doCall(custom.dynamiclogic.testunit_2.TESTUNIT_2.groovy:2)
>       at 
> custom.dynamiclogic.testunit_2.TESTUNIT_2$_run_closure1.doCall(custom.dynamiclogic.testunit_2.TESTUNIT_2.groovy)
>       at 
> java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
>       at java.base/java.lang.reflect.Method.invoke(Method.java:580)
>       at 
> org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:338)
>       at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:274)
>       at 
> org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:270)
>       at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1008)
>       at groovy.lang.Closure.call(Closure.java:471)
>       at groovy.lang.Closure.call(Closure.java:450)
>       at 
> org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:124)
>       at 
> org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:262)
>       at 
> org.codehaus.groovy.runtime.DefaultGroovyMethods.use(DefaultGroovyMethods.java:17206)
>       at org.codehaus.groovy.runtime.dgm$1263.doMethodInvoke(Unknown Source)
>       at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1235)
>       at 
> org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:344)
>       at 
> custom.dynamiclogic.testunit_2.TESTUNIT_2.run(custom.dynamiclogic.testunit_2.TESTUNIT_2.groovy:1)
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to