Browse Source

Remove guava from transitive compile classpath (#54309)

Guava was removed from Elasticsearch many years ago, but remnants of it
remain due to transitive dependencies. When a dependency pulls guava
into the compile classpath, devs can inadvertently begin using methods
from guava without realizing it. This commit moves guava to a runtime
dependency in the modules that it is needed.

Note that one special case is the html sanitizer in watcher. The third
party dep uses guava in the PolicyFactory class signature. However, only
calling a method on the PolicyFactory actually causes the class to be
loaded, a reference alone does not trigger compilation to look at the
class implementation. There we utilize a MethodHandle for invoking the
relevant method at runtime, where guava will continue to exist.
Ryan Ernst 5 years ago
parent
commit
9191c933ca
24 changed files with 123 additions and 41 deletions
  1. 1 0
      build.gradle
  2. 1 0
      buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy
  3. 3 2
      buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy
  4. 14 0
      buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy
  5. 1 1
      buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java
  6. 1 1
      distribution/tools/keystore-cli/build.gradle
  7. 1 3
      distribution/tools/plugin-cli/build.gradle
  8. 27 0
      gradle/forbidden-dependencies.gradle
  9. 1 1
      plugins/repository-azure/build.gradle
  10. 1 1
      plugins/repository-gcs/build.gradle
  11. 1 1
      plugins/repository-hdfs/build.gradle
  12. 4 0
      server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java
  13. 1 1
      x-pack/plugin/identity-provider/build.gradle
  14. 5 2
      x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java
  15. 1 2
      x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/ProcessResultsParserTests.java
  16. 1 1
      x-pack/plugin/security/build.gradle
  17. 5 1
      x-pack/plugin/security/cli/build.gradle
  18. 1 1
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java
  19. 18 12
      x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java
  20. 2 2
      x-pack/plugin/watcher/build.gradle
  21. 1 1
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java
  22. 29 5
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/HtmlSanitizer.java
  23. 2 2
      x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/SizeLimitInputStreamTests.java
  24. 1 1
      x-pack/qa/security-tools-tests/build.gradle

+ 1 - 0
build.gradle

@@ -45,6 +45,7 @@ apply from: 'gradle/build-scan.gradle'
 apply from: 'gradle/build-complete.gradle'
 apply from: 'gradle/runtime-jdk-provision.gradle'
 apply from: 'gradle/ide.gradle'
+apply from: 'gradle/forbidden-dependencies.gradle'
 apply from: 'gradle/formatting.gradle'
 
 // common maven publishing configuration

+ 1 - 0
buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy

@@ -284,6 +284,7 @@ class BuildPlugin implements Plugin<Project> {
         project.configurations.getByName(JavaPlugin.COMPILE_CONFIGURATION_NAME).dependencies.all(disableTransitiveDeps)
         project.configurations.getByName(JavaPlugin.TEST_COMPILE_CONFIGURATION_NAME).dependencies.all(disableTransitiveDeps)
         project.configurations.getByName(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME).dependencies.all(disableTransitiveDeps)
+        project.configurations.getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME).dependencies.all(disableTransitiveDeps)
     }
 
     /** Adds repositories used by ES dependencies */

+ 3 - 2
buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy

@@ -131,7 +131,8 @@ class PluginBuildPlugin implements Plugin<Project> {
         }
         createIntegTestTask(project)
         createBundleTasks(project, extension)
-        project.configurations.getByName('default').extendsFrom(project.configurations.getByName('runtime'))
+        project.configurations.getByName('default')
+            .extendsFrom(project.configurations.getByName('runtimeClasspath'))
         // allow running ES with this plugin in the foreground of a build
         project.tasks.register('run', RunTask) {
             dependsOn(project.tasks.bundlePlugin)
@@ -210,7 +211,7 @@ class PluginBuildPlugin implements Plugin<Project> {
              * that shadow jar.
              */
             from { project.plugins.hasPlugin(ShadowPlugin) ? project.shadowJar : project.jar }
-            from project.configurations.runtime - project.configurations.compileOnly
+            from project.configurations.runtimeClasspath - project.configurations.compileOnly
             // extra files for the plugin to go into the zip
             from('src/main/packaging') // TODO: move all config/bin/_size/etc into packaging
             from('src/main') {

+ 14 - 0
buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy

@@ -28,6 +28,7 @@ import org.elasticsearch.gradle.util.Util
 import org.gradle.api.JavaVersion
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
+import org.gradle.api.file.FileCollection
 import org.gradle.api.plugins.JavaBasePlugin
 import org.gradle.api.plugins.quality.Checkstyle
 import org.gradle.api.tasks.TaskProvider
@@ -142,6 +143,19 @@ class PrecommitTasks {
         ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources')
         project.tasks.withType(CheckForbiddenApis).configureEach {
             dependsOn(buildResources)
+
+            //  use the runtime classpath if we have it, but some qa projects don't have one...
+            if (name.endsWith('Test')) {
+                FileCollection runtime = project.sourceSets.test.runtimeClasspath
+                if (runtime != null) {
+                    classpath = runtime.plus(project.sourceSets.test.compileClasspath)
+                }
+            } else {
+                FileCollection runtime = project.sourceSets.main.runtimeClasspath
+                if (runtime != null) {
+                    classpath = runtime.plus(project.sourceSets.main.compileClasspath)
+                }
+            }
             targetCompatibility = BuildParams.runtimeJavaVersion.majorVersion
             if (BuildParams.runtimeJavaVersion > JavaVersion.VERSION_13) {
                 project.logger.warn(

+ 1 - 1
buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java

@@ -388,7 +388,7 @@ public class ThirdPartyAuditTask extends DefaultTask {
     }
 
     private Configuration getRuntimeConfiguration() {
-        Configuration runtime = getProject().getConfigurations().findByName("runtime");
+        Configuration runtime = getProject().getConfigurations().findByName("runtimeClasspath");
         if (runtime == null) {
             return getProject().getConfigurations().getByName("testCompile");
         }

+ 1 - 1
distribution/tools/keystore-cli/build.gradle

@@ -24,5 +24,5 @@ dependencies {
   compileOnly project(":libs:elasticsearch-cli")
   testCompile project(":test:framework")
   testCompile 'com.google.jimfs:jimfs:1.1'
-  testCompile 'com.google.guava:guava:18.0'
+  testRuntimeOnly 'com.google.guava:guava:18.0'
 }

+ 1 - 3
distribution/tools/plugin-cli/build.gradle

@@ -1,5 +1,3 @@
-import org.elasticsearch.gradle.info.BuildParams
-
 /*
  * Licensed to Elasticsearch under one or more contributor
  * license agreements. See the NOTICE file distributed with
@@ -30,7 +28,7 @@ dependencies {
   compile "org.bouncycastle:bc-fips:1.0.1"
   testCompile project(":test:framework")
   testCompile 'com.google.jimfs:jimfs:1.1'
-  testCompile 'com.google.guava:guava:18.0'
+  testRuntimeOnly 'com.google.guava:guava:18.0'
 }
 
 dependencyLicenses {

+ 27 - 0
gradle/forbidden-dependencies.gradle

@@ -0,0 +1,27 @@
+
+// we do not want any of these dependencies on the compilation classpath
+// because they could then be used within Elasticsearch
+List<String> FORBIDDEN_DEPENDENCIES = [
+  'guava'
+]
+
+Closure checkDeps = { Configuration configuration -> 
+  configuration.resolutionStrategy.eachDependency {
+    String artifactName = it.target.name
+    if (FORBIDDEN_DEPENDENCIES.contains(artifactName)) {
+      throw new GradleException("Dependency '${artifactName}' on configuration '${configuration.name}' is not allowed. " +
+        "If it is needed as a transitive depenency, try adding it to the runtime classpath")
+    }
+  }
+}
+
+subprojects {
+  if (project.path.startsWith(':test:fixtures:') || project.path.equals(':build-tools')) {
+    // fixtures are allowed to use whatever dependencies they want...
+    return
+  }
+  pluginManager.withPlugin('java') {
+    checkDeps(configurations.compileClasspath)
+    checkDeps(configurations.testCompileClasspath)
+  }
+}

+ 1 - 1
plugins/repository-azure/build.gradle

@@ -27,7 +27,7 @@ esplugin {
 dependencies {
   compile 'com.microsoft.azure:azure-storage:8.6.2'
   compile 'com.microsoft.azure:azure-keyvault-core:1.0.0'
-  compile 'com.google.guava:guava:20.0'
+  runtimeOnly 'com.google.guava:guava:20.0'
   compile 'org.apache.commons:commons-lang3:3.4'
   testCompile project(':test:fixtures:azure-fixture')
 }

+ 1 - 1
plugins/repository-gcs/build.gradle

@@ -25,7 +25,7 @@ esplugin {
 dependencies {
   compile 'com.google.cloud:google-cloud-storage:1.106.0'
   compile 'com.google.cloud:google-cloud-core:1.93.3'
-  compile 'com.google.guava:guava:26.0-jre'
+  runtimeOnly 'com.google.guava:guava:26.0-jre'
   compile 'com.google.http-client:google-http-client:1.34.2'
   compile "commons-logging:commons-logging:${versions.commonslogging}"
   compile "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}"

+ 1 - 1
plugins/repository-hdfs/build.gradle

@@ -52,7 +52,7 @@ dependencies {
   compile "org.apache.hadoop:hadoop-hdfs:${versions.hadoop2}"
   compile "org.apache.hadoop:hadoop-hdfs-client:${versions.hadoop2}"
   compile 'org.apache.htrace:htrace-core4:4.0.1-incubating'
-  compile 'com.google.guava:guava:11.0.2'
+  runtimeOnly 'com.google.guava:guava:11.0.2'
   compile 'com.google.protobuf:protobuf-java:2.5.0'
   compile 'commons-logging:commons-logging:1.1.3'
   compile "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}"

+ 4 - 0
server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java

@@ -102,4 +102,8 @@ public class Iterables {
             return it.next();
         }
     }
+
+    public static long size(Iterable<?> iterable) {
+        return StreamSupport.stream(iterable.spliterator(), true).count();
+    }
 }

+ 1 - 1
x-pack/plugin/identity-provider/build.gradle

@@ -46,7 +46,7 @@ dependencies {
   compile "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}"
   compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}"
   compile "org.apache.httpcomponents:httpclient-cache:${versions.httpclient}"
-  compile 'com.google.guava:guava:19.0'
+  runtimeOnly 'com.google.guava:guava:19.0'
 
   testCompile 'org.elasticsearch:securemock:1.2'
   testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}"

+ 5 - 2
x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ClassificationIT.java

@@ -5,7 +5,6 @@
  */
 package org.elasticsearch.xpack.ml.integration;
 
-import com.google.common.collect.Ordering;
 import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
 import org.elasticsearch.action.bulk.BulkRequestBuilder;
@@ -632,7 +631,11 @@ public class ClassificationIT extends MlNativeDataFrameAnalyticsIntegTestCase {
         // Assert that all the class probabilities lie within [0, 1] interval.
         classProbabilities.forEach(p -> assertThat(p, allOf(greaterThanOrEqualTo(0.0), lessThanOrEqualTo(1.0))));
         // Assert that the top classes are listed in the order of decreasing scores.
-        assertThat(Ordering.natural().reverse().isOrdered(classScores), is(true));
+        double prevScore = classScores.get(0);
+        for (int i = 1; i < classScores.size(); ++i) {
+            double score = classScores.get(i);
+            assertThat("class " + i, score, lessThanOrEqualTo(prevScore));
+        }
     }
 
     private <T> void assertEvaluation(String dependentVariable, List<T> dependentVariableValues, String predictedClassField) {

+ 1 - 2
x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/process/ProcessResultsParserTests.java

@@ -5,7 +5,6 @@
  */
 package org.elasticsearch.xpack.ml.process;
 
-import com.google.common.base.Charsets;
 import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
@@ -59,7 +58,7 @@ public class ProcessResultsParserTests extends ESTestCase {
     public void testParseResults() throws IOException {
         String input = "[{\"field_1\": \"a\", \"field_2\": 1.0}, {\"field_1\": \"b\", \"field_2\": 2.0},"
                 + " {\"field_1\": \"c\", \"field_2\": 3.0}]";
-        try (InputStream inputStream = new ByteArrayInputStream(input.getBytes(Charsets.UTF_8))) {
+        try (InputStream inputStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
 
             ProcessResultsParser<TestResult> parser = new ProcessResultsParser<>(TestResult.PARSER, NamedXContentRegistry.EMPTY);
             Iterator<TestResult> testResultIterator = parser.parseResults(inputStream);

+ 1 - 1
x-pack/plugin/security/build.gradle

@@ -53,7 +53,7 @@ dependencies {
     compile "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}"
     compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}"
     compile "org.apache.httpcomponents:httpclient-cache:${versions.httpclient}"
-    compile 'com.google.guava:guava:19.0'
+    runtimeOnly 'com.google.guava:guava:19.0'
 
   // Dependencies for oidc
   compile "com.nimbusds:oauth2-oidc-sdk:7.0.2"

+ 5 - 1
x-pack/plugin/security/cli/build.gradle

@@ -10,7 +10,11 @@ dependencies {
   compileOnly project(path: xpackModule('core'), configuration: 'default')
   compile "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}"
   compile "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}"
-  testImplementation 'com.google.jimfs:jimfs:1.1'
+  testImplementation('com.google.jimfs:jimfs:1.1') {
+    // this is provided by the runtime classpath, from the security project
+    exclude group: 'com.google.guava', module: 'guava'
+  }
+  testRuntimeOnly 'com.google.guava:guava:19.0'
   testCompile project(":test:framework")
   testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
 }

+ 1 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java

@@ -6,7 +6,6 @@
 
 package org.elasticsearch.xpack.security.authc;
 
-import com.google.common.collect.Sets;
 import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.action.DocWriteResponse;
 import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
@@ -23,6 +22,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.test.SecurityIntegTestCase;
 import org.elasticsearch.test.SecuritySettingsSource;

+ 18 - 12
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java

@@ -5,8 +5,6 @@
  */
 package org.elasticsearch.xpack.sql.parser;
 
-import com.google.common.base.Joiner;
-
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xpack.ql.expression.Alias;
 import org.elasticsearch.xpack.ql.expression.Literal;
@@ -214,11 +212,11 @@ public class SqlParserTests extends ESTestCase {
         // Create expression in the form of a = b OR a = b OR ... a = b
 
         // 1000 elements is ok
-        new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(1000, "a = b")));
+        new SqlParser().createExpression(join(" OR ", nCopies(1000, "a = b")));
 
         // 5000 elements cause stack overflow
         ParsingException e = expectThrows(ParsingException.class, () ->
-            new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(5000, "a = b"))));
+            new SqlParser().createExpression(join(" OR ", nCopies(5000, "a = b"))));
         assertThat(e.getMessage(),
             startsWith("line -1:0: SQL statement is too large, causing stack overflow when generating the parsing tree: ["));
     }
@@ -228,11 +226,11 @@ public class SqlParserTests extends ESTestCase {
 
         // 200 elements is ok
         new SqlParser().createExpression(
-            Joiner.on("").join(nCopies(200, "abs(")).concat("i").concat(Joiner.on("").join(nCopies(200, ")"))));
+            join("", nCopies(200, "abs(")).concat("i").concat(join("", nCopies(200, ")"))));
 
         // 5000 elements cause stack overflow
         ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createExpression(
-            Joiner.on("").join(nCopies(1000, "abs(")).concat("i").concat(Joiner.on("").join(nCopies(1000, ")")))));
+            join("", nCopies(1000, "abs(")).concat("i").concat(join("", nCopies(1000, ")")))));
         assertThat(e.getMessage(),
             startsWith("line -1:0: SQL statement is too large, causing stack overflow when generating the parsing tree: ["));
     }
@@ -241,11 +239,11 @@ public class SqlParserTests extends ESTestCase {
         // Create expression in the form of a + a + a + ... + a
 
         // 1000 elements is ok
-        new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(1000, "a")));
+        new SqlParser().createExpression(join(" + ", nCopies(1000, "a")));
 
         // 5000 elements cause stack overflow
         ParsingException e = expectThrows(ParsingException.class, () ->
-            new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(5000, "a"))));
+            new SqlParser().createExpression(join(" + ", nCopies(5000, "a"))));
         assertThat(e.getMessage(),
             startsWith("line -1:0: SQL statement is too large, causing stack overflow when generating the parsing tree: ["));
     }
@@ -255,15 +253,15 @@ public class SqlParserTests extends ESTestCase {
 
         // 200 elements is ok
         new SqlParser().createStatement(
-            Joiner.on(" (").join(nCopies(200, "SELECT * FROM"))
+            join(" (", nCopies(200, "SELECT * FROM"))
                 .concat("t")
-                .concat(Joiner.on("").join(nCopies(199, ")"))));
+                .concat(join("", nCopies(199, ")"))));
 
         // 500 elements cause stack overflow
         ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createStatement(
-            Joiner.on(" (").join(nCopies(500, "SELECT * FROM"))
+            join(" (", nCopies(500, "SELECT * FROM"))
                 .concat("t")
-                .concat(Joiner.on("").join(nCopies(499, ")")))));
+                .concat(join("", nCopies(499, ")")))));
         assertThat(e.getMessage(),
             startsWith("line -1:0: SQL statement is too large, causing stack overflow when generating the parsing tree: ["));
     }
@@ -308,4 +306,12 @@ public class SqlParserTests extends ESTestCase {
         String dirStr = dir.toString();
         return randomBoolean() && dirStr.equals("ASC") ? "" : " " + dirStr;
     }
+
+    private String join(String delimiter, Iterable<String> strings) {
+        StringJoiner joiner = new StringJoiner(delimiter);
+        for (String s : strings) {
+            joiner.add(s);
+        }
+        return joiner.toString();
+    }
 }

+ 2 - 2
x-pack/plugin/watcher/build.gradle

@@ -33,8 +33,8 @@ dependencies {
 
   // watcher deps
   compile 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20191001.1'
-  compile 'com.google.guava:guava:27.1-jre' // needed by watcher for the html sanitizer
-  compile 'com.google.guava:failureaccess:1.0.1'
+  runtimeOnly 'com.google.guava:guava:27.1-jre' // needed by watcher for the html sanitizer
+  runtimeOnly 'com.google.guava:failureaccess:1.0.1'
   compile 'com.sun.mail:jakarta.mail:1.6.4'
   compile 'com.sun.activation:jakarta.activation:1.2.1'
   compileOnly "org.apache.httpcomponents:httpclient:${versions.httpclient}"

+ 1 - 1
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java

@@ -5,7 +5,6 @@
  */
 package org.elasticsearch.xpack.watcher.execution;
 
-import com.google.common.collect.Iterables;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.message.ParameterizedMessage;
@@ -31,6 +30,7 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.common.util.iterable.Iterables;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;

+ 29 - 5
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/HtmlSanitizer.java

@@ -14,12 +14,16 @@ import org.owasp.html.ElementPolicy;
 import org.owasp.html.HtmlPolicyBuilder;
 import org.owasp.html.PolicyFactory;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Function;
+import java.util.function.UnaryOperator;
 
 public class HtmlSanitizer {
 
@@ -47,23 +51,43 @@ public class HtmlSanitizer {
     private static Setting<List<String>> SETTING_SANITIZATION_DISALLOW =
             Setting.listSetting("xpack.notification.email.html.sanitization.disallow", Collections.emptyList(), Function.identity(),
                     Property.NodeScope);
+    private static final MethodHandle sanitizeHandle;
+    static {
+        try {
+            MethodHandles.Lookup methodLookup = MethodHandles.publicLookup();
+            MethodType sanitizeSignature = MethodType.methodType(String.class, String.class);
+            sanitizeHandle = methodLookup.findVirtual(PolicyFactory.class, "sanitize", sanitizeSignature);
+        } catch (NoSuchMethodException|IllegalAccessException e) {
+            throw new RuntimeException("Missing guava on runtime classpath", e);
+        }
+    }
 
     private final boolean enabled;
-    @SuppressForbidden( reason = "PolicyFactory uses guava Function")
-    private final PolicyFactory policy;
-    
+    private final UnaryOperator<String> sanitizer;
+
     public HtmlSanitizer(Settings settings) {
         enabled = SETTING_SANITIZATION_ENABLED.get(settings);
         List<String> allow = SETTING_SANITIZATION_ALLOW.get(settings);
         List<String> disallow = SETTING_SANITIZATION_DISALLOW.get(settings);
-        policy = createCommonPolicy(allow, disallow);
+
+        // The sanitize method of PolicyFactory pulls in guava dependencies, which we want to isolate to
+        // runtime only rather than compile time where more guava uses can be accidentally pulled in.
+        // Here we lookup the sanitize method at runtime and grab a method handle to invoke.
+        PolicyFactory policy = createCommonPolicy(allow, disallow);
+        sanitizer = s -> {
+            try {
+                return (String) sanitizeHandle.invokeExact(policy, s);
+            } catch (Throwable e) {
+                throw new RuntimeException("Failed to invoke sanitize method of PolicyFactory", e);
+            }
+        };
     }
 
     public String sanitize(String html) {
         if (!enabled) {
             return html;
         }
-        return policy.sanitize(html);
+        return sanitizer.apply(html);
     }
 
     @SuppressForbidden( reason = "PolicyFactory uses guava Function")

+ 2 - 2
x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/common/http/SizeLimitInputStreamTests.java

@@ -12,7 +12,7 @@ import org.elasticsearch.test.ESTestCase;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 
-import static com.google.common.base.Charsets.UTF_8;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.Matchers.is;
 
 public class SizeLimitInputStreamTests extends ESTestCase {
@@ -52,4 +52,4 @@ public class SizeLimitInputStreamTests extends ESTestCase {
             }
         }
     }
-}
+}

+ 1 - 1
x-pack/qa/security-tools-tests/build.gradle

@@ -4,7 +4,7 @@ dependencies {
   testCompile project(xpackModule('security'))
   testCompile project(path: xpackModule('security'), configuration: 'testArtifacts')
   testCompile 'com.google.jimfs:jimfs:1.1'
-  testCompile 'com.google.guava:guava:16.0.1'
+  testRuntimeOnly 'com.google.guava:guava:16.0.1'
 }
 
 // add test resources from security, so certificate tool tests can use example certs