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

Plug-in calling Jenkins REST API fails with 403 when configured with pluginFirstClassLoader = true

      Enabling pluginFirstClassLoader for conflicting dependencies seems to affect the plug-in's capability to invoke Jenkins REST endpoints. 

      Statistics gatherer plug-in currently uses GSON 2.8.6 (through unirest). This conflicts with Jenkins GIT plug-in (4.2.2), that currently uses GSON 2.8.2.  These two versions are incompatible. To get around the GSON conflict, we recompiled the plug-in with pluginFirstClassLoader enabled as per guidance found here https://www.jenkins.io/doc/developer/plugin-development/dependencies-and-class-loading.This however seems to affect the security context of the plug-in as it is now gets an error calling an API endpoint on Jenkins. 

      More detail: 

      Tested on: 

      • Jenkins 2.222.3 
      • Jenkins 2.190.2

      Before enabling pluginFirstClassLoader: 

      java.lang.NoSuchMethodError: com.google.gson.Gson.newBuilder()Lcom/google/gson/GsonBuilder;
      at kong.unirest.json.JSONElement.<clinit>(JSONElement.java:39)
      at kong.unirest.JsonNode.<init>(JsonNode.java:44)
      at kong.unirest.JsonResponse.toJsonNode(JsonResponse.java:49)
      at kong.unirest.JsonResponse.getNode(JsonResponse.java:43)
      at kong.unirest.JsonResponse.<init>(JsonResponse.java:35)
      at kong.unirest.apache.BaseApacheClient.transformBody(BaseApacheClient.java:53)
      at kong.unirest.apache.ApacheClient.request(ApacheClient.java:127)
      at kong.unirest.BaseRequest.asJson(BaseRequest.java:232)
      at org.jenkins.plugins.statistics.gatherer.util.RestClientUtil.getJson(RestClientUtil.java:79)
      at org.jenkins.plugins.statistics.gatherer.listeners.RunStatsListener.addBuildFailureCauses(RunStatsListener.java:322)

      Enabling pluginFirstClassLoader:

      Added to pom.xml for plug-in and recompiled: 

      <build>
       <plugins>
       <plugin>
       <groupId>org.jenkins-ci.tools</groupId>
       <artifactId>maven-hpi-plugin</artifactId>
       <configuration>
       <minimumJavaVersion>1.8</minimumJavaVersion>
       <pluginFirstClassLoader>true</pluginFirstClassLoader>
       </configuration>
       </plugin>

      After enabling pluginFirstClassLoader:

      java.lang.NullPointerException at org.jenkins.plugins.statistics.gatherer.util.RestClientUtil.getJson(RestClientUtil.java:86) at org.jenkins.plugins.statistics.gatherer.listeners.RunStatsListener.addBuildFailureCauses(RunStatsListener.java:326) at org.jenkins.plugins.statistics.gatherer.listeners.RunStatsListener.onFinalized(RunStatsListener.java:304) at hudson.model.listeners.RunListener.fireFinalized(RunListener.java:255)

      Logged out the URL it is attempting to invoke: 

      https://<hidden>/jenkins/android-digital/job/Carl_BFA_Test/7/api/json?depth=2&tree=actions[foundFailureCauses[categories,description,id,name]]

      Logging out the HTTP status reason for the call: 

      JSON status: Forbidden

      Verified that this URL can be manually invoked when signed in and returns results.  

          [JENKINS-62681] Plug-in calling Jenkins REST API fails with 403 when configured with pluginFirstClassLoader = true

          Daniel Beck added a comment -

          Pretty much inherent I think, and the best solution I'm aware of would be the creation of a "Gson plugin".

          Poking jglick to confirm.

          CC markewaite

          Daniel Beck added a comment - Pretty much inherent I think, and the best solution I'm aware of would be the creation of a "Gson plugin". Poking jglick to confirm. CC markewaite

          Mark Waite added a comment -

          The gson dependency is a transitive dependency of the LFS module inside JGit. The JGit LFS module is a separate module that has been included in the hopes that we'll eventually be able to support large files from JGit in addition to the large file support we have with command line git.

          Since large file support is not yet implemented for JGit, we could exclude the JGit LFS module until the git client plugin implementation of LFS support is ready to use JGit LFS. That would resolve this specific issue by temporarily removing the use of gson from the git client plugin

          Mark Waite added a comment - The gson dependency is a transitive dependency of the LFS module inside JGit. The JGit LFS module is a separate module that has been included in the hopes that we'll eventually be able to support large files from JGit in addition to the large file support we have with command line git. Since large file support is not yet implemented for JGit, we could exclude the JGit LFS module until the git client plugin implementation of LFS support is ready to use JGit LFS. That would resolve this specific issue by temporarily removing the use of gson from the git client plugin

          Oleg Nenashev added a comment -

          IMO pluginFirstClassLoader = true is a hack, not a feature. Whatever is the fix, I believe the Statistics Gatherer plugin needs to be fixed to avoid this flag

          I support creating a GSON API plugin for this matter. Taking the number of usages in https://github.com/search?l=Maven+POM&q=org%3Ajenkinsci+gson&type=Code , it is a matter of time till something else explodes. CC loghijiaha markyjackson5 since the Machine Learning plugin is one of the plugins at risk.

           

           

          Oleg Nenashev added a comment - IMO pluginFirstClassLoader = true is a hack, not a feature. Whatever is the fix, I believe the Statistics Gatherer plugin needs to be fixed to avoid this flag I support creating a GSON API plugin for this matter. Taking the number of usages in  https://github.com/search?l=Maven+POM&q=org%3Ajenkinsci+gson&type=Code  , it is a matter of time till something else explodes. CC loghijiaha   markyjackson5 since the Machine Learning plugin is one of the plugins at risk.    

          Jesse Glick added a comment - - edited

          Unless you have a dependency on the git plugin, you do not need to specify pluginFirstClassLoader or use shading. Not sure what would have caused your NoSuchMethodError; is this from a real installation, or just mvn hpi:run or JenkinsRule both of which use an unrealistic class loading environment?

          The NullPointerException as well would be some bug in your plugin.

          A GSON library wrapper plugin may be useful for various reasons (such as having only a single plugin to update in case of a security advisory), but whatever your issues are, they should be solvable without one.

          Jesse Glick added a comment - - edited Unless you have a dependency on the git plugin, you do not need to specify pluginFirstClassLoader or use shading. Not sure what would have caused your NoSuchMethodError ; is this from a real installation, or just mvn hpi:run or JenkinsRule both of which use an unrealistic class loading environment? The NullPointerException as well would be some bug in your plugin. A GSON library wrapper plugin may be useful for various reasons (such as having only a single plugin to update in case of a security advisory), but whatever your issues are, they should be solvable without one.

          Carl Meyer added a comment - - edited

          Hi, 

          Some more updates on this one. As an extension on the solution, we added an API token configuration parameter to Statistics Loader to perform authenticated requests. This felt like a bit of a patchwork solution, and would result in some more ClassLoader failures further down the stack.

          jglick: The GIT plug-in is used in the Statistics Gatherer Plug-in through the usage of: hudson.plugins.git.util.BuildData in RunStatsListener.java to obtain SCM information for the build. 

          We then forced a hard dependency between Statistics Class Loader and Build Failure Analyzer as a means to invoke Build Failure Analyzer directly and avoid the GSON dependency for this code path: 

          In Statistics Gatherer Plugin, pom.xml:

          <dependency>
           <groupId>com.sonyericsson.jenkins.plugins.bfa</groupId>
           <artifactId>build-failure-analyzer</artifactId>
           <version>1.26.0</version>
          </dependency>
          

          Also update unirest to 3.6.00+ (fixes memory leak as mentioned below): 

          <dependency>
            <groupId>com.konghq</groupId>
            <artifactId>unirest-java</artifactId>
            <version>3.6.00</version>
          </dependency>

          In RunStatsListener.java, replace addFailureCauses with:  

              private void addBuildFailureCauses(Run run, BuildStats build) {
                  // Check for the Build Failure Analyzer plugin
                  List<PluginWrapper> plugins = Jenkins.getInstanceOrNull().getPluginManager().getPlugins();
                  for (PluginWrapper plugin : plugins) {
                      if (plugin.getDisplayName().contains("Build Failure Analyzer")) {
                          FailureCauseBuildAction action = run.getAction(FailureCauseBuildAction.class);
                          if (action != null) {
                              List<Map> failureCauses = new ArrayList<>();
                              List<FoundFailureCause> foundFailureCausesList = action.getFoundFailureCauses();
                              if (foundFailureCausesList != null && foundFailureCausesList.size() != 0) {
                                  for (FoundFailureCause failureCauseItem : foundFailureCausesList) {
                                      LOGGER.info("Found failure cause: " + failureCauseItem.getDescription());
                                      JSONObject failureCauseJSONObject = new JSONObject();                            failureCauseJSONObject.put("id", failureCauseItem.getId());
                                      failureCauseJSONObject.put("name", failureCauseItem.getName());
                                      failureCauseJSONObject.put("description", failureCauseItem.getDescription());                            List<String> categories = failureCauseItem.getCategories();
                                      if (categories != null)
                                          failureCauseJSONObject.put("categories", categories);                            Map jsonObject = JSONUtil.convertBuildFailureToMap(failureCauseJSONObject);
                                      failureCauses.add(jsonObject);
                                  }
                              }
                              build.setBuildFailureCauses(failureCauses);
                          } else {
                              LOGGER.warning("Cannot find failure causes");
                          }
                          break;
                      }
                  }
              }
          

          In JSONUtil.java, replace Map with (also fixed NullPointerException as listed below): 

              public static Map<String, Object> convertBuildFailureToMap(JSONObject jObject){
                  Map<String, Object> map = new HashMap<>();
                  Iterator<?> keys = jObject.keys();        while( keys.hasNext() ){
                      String key = (String)keys.next();
                      if ("categories".equals(key)){
                          // Do not attempt to add empty categories.
                          boolean isEmpty = jObject.isNull(key) || jObject.isNull(key);
                          if (!isEmpty) {
                              List<String> value = convertJsonArrayToList(jObject.getJSONArray(key));
                              map.put(key, value);
                          }
                      } else {
                          String value = jObject.getString(key);
                          map.put(key, value);
                      }
                  }
                  return map;
              }

          I am not familiar with the plug-in support policy, but would advise some warning be listed against use of the Statistics Gatherer Plug-i as we found a number of other issues with this plug-in (in it's original form), such as: 

          • The logic of JSON processing fails if Build Failure Analyzer does not supply the optional categories parameter with a NullPointerException
          • The version of unirest that the plug-in uses has known memory leaks

          Hope this helps. 

          Carl Meyer added a comment - - edited Hi,  Some more updates on this one. As an extension on the solution, we added an API token configuration parameter to Statistics Loader to perform authenticated requests. This felt like a bit of a patchwork solution, and would result in some more ClassLoader failures further down the stack. jglick : The GIT plug-in is used in the Statistics Gatherer Plug-in through the usage of:  hudson.plugins.git.util.BuildData  in RunStatsListener.java to obtain SCM information for the build.  We then forced a hard dependency between Statistics Class Loader and Build Failure Analyzer as a means to invoke Build Failure Analyzer directly and avoid the GSON dependency for this code path:  In Statistics Gatherer Plugin, pom.xml : <dependency> <groupId>com.sonyericsson.jenkins.plugins.bfa</groupId> <artifactId>build-failure-analyzer</artifactId> <version>1.26.0</version> </dependency> Also update unirest to 3.6.00+ (fixes memory leak as mentioned below):  <dependency> <groupId>com.konghq</groupId> <artifactId>unirest-java</artifactId> <version>3.6.00</version> </dependency> In RunStatsListener.java , replace addFailureCauses with:   private void addBuildFailureCauses(Run run, BuildStats build) { // Check for the Build Failure Analyzer plugin List<PluginWrapper> plugins = Jenkins.getInstanceOrNull().getPluginManager().getPlugins(); for (PluginWrapper plugin : plugins) { if (plugin.getDisplayName().contains( "Build Failure Analyzer" )) { FailureCauseBuildAction action = run.getAction(FailureCauseBuildAction.class); if (action != null ) { List<Map> failureCauses = new ArrayList<>(); List<FoundFailureCause> foundFailureCausesList = action.getFoundFailureCauses(); if (foundFailureCausesList != null && foundFailureCausesList.size() != 0) { for (FoundFailureCause failureCauseItem : foundFailureCausesList) { LOGGER.info( "Found failure cause: " + failureCauseItem.getDescription()); JSONObject failureCauseJSONObject = new JSONObject(); failureCauseJSONObject.put( "id" , failureCauseItem.getId()); failureCauseJSONObject.put( "name" , failureCauseItem.getName()); failureCauseJSONObject.put( "description" , failureCauseItem.getDescription()); List< String > categories = failureCauseItem.getCategories(); if (categories != null ) failureCauseJSONObject.put( "categories" , categories); Map jsonObject = JSONUtil.convertBuildFailureToMap(failureCauseJSONObject); failureCauses.add(jsonObject); } } build.setBuildFailureCauses(failureCauses); } else { LOGGER.warning( "Cannot find failure causes" ); } break ; } } } In JSONUtil.java , replace Map with (also fixed NullPointerException as listed below):  public static Map< String , Object > convertBuildFailureToMap(JSONObject jObject){ Map< String , Object > map = new HashMap<>(); Iterator<?> keys = jObject.keys(); while ( keys.hasNext() ){ String key = ( String )keys.next(); if ( "categories" .equals(key)){ // Do not attempt to add empty categories. boolean isEmpty = jObject.isNull(key) || jObject.isNull(key); if (!isEmpty) { List< String > value = convertJsonArrayToList(jObject.getJSONArray(key)); map.put(key, value); } } else { String value = jObject.getString(key); map.put(key, value); } } return map; } I am not familiar with the plug-in support policy, but would advise some warning be listed against use of the Statistics Gatherer Plug-i as we found a number of other issues with this plug-in (in it's original form), such as:  The logic of JSON processing fails if Build Failure Analyzer does not supply the optional categories parameter with a NullPointerException The version of unirest that the plug-in uses has known memory leaks Hope this helps. 

            carlmeyer Carl Meyer
            carlmeyer Carl Meyer
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: