Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-42129

Closure delegate idiom fails attempting to call .DefaultGroovyMethods.invokeMethod(Object, String, Object)

      I have a simple Jenkinsfile that uses a (global) library method to wrap a closure. In the closure, I want to be able to optionally set some values that are picked up by the wrapper and acted on.

      Things work fine if I don't set the delegate on the closure. But when I do, I get the following build failure due to security violation when trying to run the 'node' step.

      [Pipeline] catchError
      [Pipeline] {
      [Pipeline] }
      org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:192)
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:95)
      	at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:149)
      	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:146)
      	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:123)
      	at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:16)
      	at WorkflowScript.run(WorkflowScript:6)
      	at emailBreaks.call(/home/jenkins/jobs/build-failures/builds/21/libs/myPipelineLib/vars/emailBreaks.groovy:9)
      	at ___cps.transform___(Native Method)
      	at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:57)
      	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.dispatchOrArg(FunctionCallBlock.java:109)
      	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.fixArg(FunctionCallBlock.java:82)
      	at sun.reflect.GeneratedMethodAccessor379.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      	at java.lang.reflect.Method.invoke(Method.java:498)
      	at com.cloudbees.groovy.cps.impl.ContinuationPtr$ContinuationImpl.receive(ContinuationPtr.java:72)
      	at com.cloudbees.groovy.cps.impl.ClosureBlock.eval(ClosureBlock.java:46)
      	at com.cloudbees.groovy.cps.Next.step(Next.java:74)
      	at com.cloudbees.groovy.cps.Continuable.run0(Continuable.java:154)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.access$001(SandboxContinuable.java:18)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable$1.call(SandboxContinuable.java:33)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable$1.call(SandboxContinuable.java:30)
      	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox.runInSandbox(GroovySandbox.java:108)
      	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.run0(SandboxContinuable.java:30)
      	at org.jenkinsci.plugins.workflow.cps.CpsThread.runNextChunk(CpsThread.java:163)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:328)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$100(CpsThreadGroup.java:80)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:240)
      	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:228)
      	at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:63)
      	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
      	at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:112)
      	at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
      	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
      	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
      	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
      	at java.lang.Thread.run(Thread.java:745)
      [Pipeline] // catchError
      [Pipeline] node
      Running on master in /home/jenkins/workspace/build-failures
      [Pipeline] {
      [Pipeline] emailextrecipients
      [Pipeline] step
      Sending e-mails to: user@example.com
      [Pipeline] }
      [Pipeline] // node
      [Pipeline] End of Pipeline
      Finished: FAILURE
      

          [JENKINS-42129] Closure delegate idiom fails attempting to call .DefaultGroovyMethods.invokeMethod(Object, String, Object)

          Richard Lee added a comment -

          Note that it seems that the resolveStrategy is the key item. If set to Closure.DELEGATE_FIRST, you get the security violation. If left at the default (owner first), no security violation. Of course, the code doesn't work correctly in that case.

          Richard Lee added a comment - Note that it seems that the resolveStrategy is the key item. If set to Closure.DELEGATE_FIRST, you get the security violation. If left at the default (owner first), no security violation. Of course, the code doesn't work correctly in that case.

          Jesse Glick added a comment -

          No idea offhand, would need to spend time digging into what Groovy is trying to call and why the sandbox interceptor is misinterpreting that.

          In general I do not advise use of the closure-delegate idiom in Pipeline libraries. It is subtle and easy to get wrong. Better to have a plain old method taking positional or named (~ map) arguments, some of which may be of type Closure. Something like (untested):

          def call(Closure body, String mailingList = 'builds-list@example.com') {
              catchError {
                  body()
              }
          
              (currentBuild.result != 'ABORTED') && node('master') {
                  // send e-mail notifications for failed or unstable builds
                  def to = emailextrecipients([
                      [$class: 'CulpritsRecipientProvider'],
                      [$class: 'DevelopersRecipientProvider'],
                      [$class: 'RequesterRecipientProvider'],
                      [$class: 'UpstreamComitterRecipientProvider']
                  ]) + ' ' + mailingList
                  step([$class: 'Mailer',
                      notifyEveryUnstableBuild: true,
                      recipients: to
                  ])
              }
          }
          

          Jesse Glick added a comment - No idea offhand, would need to spend time digging into what Groovy is trying to call and why the sandbox interceptor is misinterpreting that. In general I do not advise use of the closure-delegate idiom in Pipeline libraries. It is subtle and easy to get wrong. Better to have a plain old method taking positional or named (~ map) arguments, some of which may be of type Closure . Something like (untested): def call(Closure body, String mailingList = 'builds-list@example.com' ) { catchError { body() } (currentBuild.result != 'ABORTED' ) && node( 'master' ) { // send e-mail notifications for failed or unstable builds def to = emailextrecipients([ [$class: 'CulpritsRecipientProvider' ], [$class: 'DevelopersRecipientProvider' ], [$class: 'RequesterRecipientProvider' ], [$class: 'UpstreamComitterRecipientProvider' ] ]) + ' ' + mailingList step([$class: 'Mailer' , notifyEveryUnstableBuild: true , recipients: to ]) } }

          I'm also running into this issue and it's easily reproducible without a pipeline library (although, that's where I ran into it initially).

          def myPipeline(body = {}) {
              def config = [
                  preBuild: { echo "bye $it" }
              ]
          
              body.resolveStrategy = Closure.DELEGATE_FIRST
              body.delegate = config
              body()
          
              config.preBuild?.call("world")
          }
          
          myPipeline()
          
          myPipeline {
              preBuild = { echo "hello $it" }
          }
          

          I'm expecting the script to output:

          bye world
          hello world

           Instead, we see "bye world" followed by an exception. 
          org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
          To work around this, and a possible pointer to the fix, is to use the this object. For example, this works:

          myPipeline {
              preBuild = { this.echo "hello $it" }
          }

          I believe this works because  this points to the enclosing class where all the pipeline "globals" are still in the enclosing class's Binding.

          Steve Prentice added a comment - I'm also running into this issue and it's easily reproducible without a pipeline library (although, that's where I ran into it initially). def myPipeline(body = {}) { def config = [ preBuild: { echo "bye $it" } ] body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config body() config.preBuild?.call( "world" ) } myPipeline() myPipeline { preBuild = { echo "hello $it" } } I'm expecting the script to output: bye world hello world  Instead, we see "bye world" followed by an exception.  org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object To work around this, and a possible pointer to the fix, is to use the this object. For example, this works: myPipeline { preBuild = { this .echo "hello $it" } } I believe this works because  this points to the enclosing class  where all the pipeline "globals" are still in the enclosing class's Binding.

          Andrew Bayer added a comment -

          In that example, at least, preBuild needs its delegate set as well - when I change to this:

              body.resolveStrategy = Closure.DELEGATE_FIRST
              body.delegate = config
              body()
          
              if (config.preBuild != null) {
                  config.preBuild.resolveStrategy = Closure.DELEGATE_FIRST
                  config.preBuild.delegate = this
              }
              config.preBuild?.call("world")
          

          ...it works.

          In the original example, I believe you need to change the setup to something more like the second example - passing in the closure to run as a key/value pair itself. I don't think you can effectively mix closures to execute and variables to "return" like that.

          Andrew Bayer added a comment - In that example, at least, preBuild needs its delegate set as well - when I change to this: body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config body() if (config.preBuild != null ) { config.preBuild.resolveStrategy = Closure.DELEGATE_FIRST config.preBuild.delegate = this } config.preBuild?.call( "world" ) ...it works. In the original example, I believe you need to change the setup to something more like the second example - passing in the closure to run as a key/value pair itself. I don't think you can effectively mix closures to execute and variables to "return" like that.

          abayer, thanks for the workaround/solution! Works as you noted.

          Steve Prentice added a comment - abayer , thanks for the workaround/solution! Works as you noted.

          pixman20 added a comment - - edited

          abayer, I don't think the original issue in the description was resolved, only the later example in the comments.

          In the original example attached (copied below), the "sh" line is getting this error as shown in the description

          @Library('myPipelineLib') _
          
          emailBreaks {
              mailingList = 'user@example.com'
              
              node('master') {
                  git 'https://git.example.org/user/pipeline-build-failures.git'
                  //sh 'true'
                  sh 'might fail' // <-- Causes: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang.Object java.lang.String java.lang.Object
              }
          }
          

          Looking at the emailBreaks shared pipeline function, I'm not sure how your previous solution applies (I could easily just be missing it)

          def call(Closure body) {
              currentBuild.result = 'SUCCESS'
          
              def config = [:]
              body.resolveStrategy = Closure.DELEGATE_FIRST
              body.delegate = config
              
              catchError {
                  body() // <-- Running the closure here delegates to config, but seems to make anything available in the owner's scope a script approval problem
              }
          
              ...
          }
           

          I have the exact same situation as this, error and all.
          I can workaround it (painfully so), by explicitly defining the object containing the function that's being invoked.
          For example, using the above example, changing "sh" to either "this.sh" or "steps.sh" would work probably work (it was fixed in my case by adding the global library name)
          For some reason, when letting the scope with the defined function be found implicitly, the script approval part gets angry

          pixman20 added a comment - - edited abayer , I don't think the original issue in the description was resolved, only the later example in the comments. In the original example attached (copied below), the "sh" line is getting this error as shown in the description @Library( 'myPipelineLib' ) _ emailBreaks { mailingList = 'user@example.com' node( 'master' ) { git 'https: //git.example.org/user/pipeline-build-failures.git' //sh ' true ' sh 'might fail' // <-- Causes: Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods invokeMethod java.lang. Object java.lang. String java.lang. Object } } Looking at the emailBreaks shared pipeline function, I'm not sure how your previous solution applies (I could easily just be missing it) def call(Closure body) { currentBuild.result = 'SUCCESS' def config = [:] body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config catchError { body() // <-- Running the closure here delegates to config, but seems to make anything available in the owner's scope a script approval problem } ... }   I have the exact same situation as this, error and all. I can workaround it (painfully so), by explicitly defining the object containing the function that's being invoked. For example, using the above example, changing "sh" to either "this.sh" or "steps.sh" would work probably work (it was fixed in my case by adding the global library name) For some reason, when letting the scope with the defined function be found implicitly, the script approval part gets angry

          The original issue is definitely not resolved. The example from pixman is still not working which indicates that the resolveStrategy is not handled correctly for pipelines / libraries.

          Wolfgang Issovits added a comment - The original issue is definitely not resolved. The example from pixman is still not working which indicates that the resolveStrategy is not handled correctly for pipelines / libraries.

            Unassigned Unassigned
            llamahunter Richard Lee
            Votes:
            4 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated: