diff --git a/core/pom.xml b/core/pom.xml index c30d8bd38c2..bc871f79963 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -236,7 +236,7 @@ org/elasticsearch/test/**/* org/elasticsearch/bootstrap/BootstrapForTesting.class - org/elasticsearch/bootstrap/MockPluginPolicy.class + org/elasticsearch/bootstrap/BootstrapForTesting$*.class org/elasticsearch/common/cli/CliToolTestCase.class org/elasticsearch/common/cli/CliToolTestCase$*.class @@ -265,7 +265,7 @@ rest-api-spec/**/* org/elasticsearch/test/**/* org/elasticsearch/bootstrap/BootstrapForTesting.class - org/elasticsearch/bootstrap/MockPluginPolicy.class + org/elasticsearch/bootstrap/BootstrapForTesting$*.class org/elasticsearch/common/cli/CliToolTestCase.class org/elasticsearch/common/cli/CliToolTestCase$*.class org/elasticsearch/cluster/MockInternalClusterInfoService.class diff --git a/core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java b/core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java index 09b4a4d0d94..b6ebd15face 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java @@ -21,14 +21,12 @@ package org.elasticsearch.bootstrap; import org.elasticsearch.common.SuppressForbidden; -import java.net.URI; import java.net.URL; import java.security.CodeSource; import java.security.Permission; import java.security.PermissionCollection; import java.security.Policy; import java.security.ProtectionDomain; -import java.security.URIParameter; import java.util.Map; /** custom policy for union of static and dynamic permissions */ @@ -42,13 +40,11 @@ final class ESPolicy extends Policy { final Policy template; final Policy untrusted; final PermissionCollection dynamic; - final Map plugins; + final Map plugins; - public ESPolicy(PermissionCollection dynamic, Map plugins) throws Exception { - URI policyUri = getClass().getResource(POLICY_RESOURCE).toURI(); - URI untrustedUri = getClass().getResource(UNTRUSTED_RESOURCE).toURI(); - this.template = Policy.getInstance("JavaPolicy", new URIParameter(policyUri)); - this.untrusted = Policy.getInstance("JavaPolicy", new URIParameter(untrustedUri)); + public ESPolicy(PermissionCollection dynamic, Map plugins) { + this.template = Security.readPolicy(getClass().getResource(POLICY_RESOURCE), JarHell.parseClassPath()); + this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), new URL[0]); this.dynamic = dynamic; this.plugins = plugins; } @@ -69,9 +65,10 @@ final class ESPolicy extends Policy { if (BootstrapInfo.UNTRUSTED_CODEBASE.equals(location.getFile())) { return untrusted.implies(domain, permission); } - // check for an additional plugin permission - PermissionCollection plugin = plugins.get(location.getFile()); - if (plugin != null && plugin.implies(permission)) { + // check for an additional plugin permission: plugin policy is + // only consulted for its codesources. + Policy plugin = plugins.get(location.getFile()); + if (plugin != null && plugin.implies(domain, permission)) { return true; } } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Security.java b/core/src/main/java/org/elasticsearch/bootstrap/Security.java index 26ae76b8b37..2208eea7d2e 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Security.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Security.java @@ -20,10 +20,12 @@ package org.elasticsearch.bootstrap; import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.PluginInfo; import java.io.*; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.AccessMode; import java.nio.file.DirectoryStream; @@ -32,15 +34,14 @@ import java.nio.file.Files; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; -import java.security.PermissionCollection; import java.security.Permissions; import java.security.Policy; import java.security.URIParameter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; -import java.util.regex.Pattern; /** * Initializes SecurityManager with necessary permissions. @@ -99,8 +100,6 @@ final class Security { * Can only happen once! */ static void configure(Environment environment) throws Exception { - // set properties for jar locations - setCodebaseProperties(); // enable security policy: union of template and environment-based paths, and possibly plugin permissions Policy.setPolicy(new ESPolicy(createPermissions(environment), getPluginPermissions(environment))); @@ -121,70 +120,34 @@ final class Security { selfTest(); } - // mapping of jars to codebase properties - // note that this is only read once, when policy is parsed. - private static final Map SPECIAL_JARS; - static { - Map m = new IdentityHashMap<>(); - m.put(Pattern.compile(".*lucene-core-.*\\.jar$"), "es.security.jar.lucene.core"); - m.put(Pattern.compile(".*lucene-test-framework-.*\\.jar$"), "es.security.jar.lucene.testframework"); - m.put(Pattern.compile(".*randomizedtesting-runner-.*\\.jar$"), "es.security.jar.randomizedtesting.runner"); - m.put(Pattern.compile(".*junit4-ant-.*\\.jar$"), "es.security.jar.randomizedtesting.junit4"); - m.put(Pattern.compile(".*securemock-.*\\.jar$"), "es.security.jar.elasticsearch.securemock"); - SPECIAL_JARS = Collections.unmodifiableMap(m); - } - - /** - * Sets properties (codebase URLs) for policy files. - * JAR locations are not fixed so we have to find the locations of - * the ones we want. - */ - @SuppressForbidden(reason = "proper use of URL") - static void setCodebaseProperties() { - for (URL url : JarHell.parseClassPath()) { - for (Map.Entry e : SPECIAL_JARS.entrySet()) { - if (e.getKey().matcher(url.getPath()).matches()) { - String prop = e.getValue(); - if (System.getProperty(prop) != null) { - throw new IllegalStateException("property: " + prop + " is unexpectedly set: " + System.getProperty(prop)); - } - System.setProperty(prop, url.toString()); - } - } - } - for (String prop : SPECIAL_JARS.values()) { - if (System.getProperty(prop) == null) { - System.setProperty(prop, "file:/dev/null"); // no chance to be interpreted as "all" - } - } - } - /** * Sets properties (codebase URLs) for policy files. * we look for matching plugins and set URLs to fit */ @SuppressForbidden(reason = "proper use of URL") - static Map getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException { - Map map = new HashMap<>(); + static Map getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException { + Map map = new HashMap<>(); if (Files.exists(environment.pluginsFile())) { try (DirectoryStream stream = Files.newDirectoryStream(environment.pluginsFile())) { for (Path plugin : stream) { Path policyFile = plugin.resolve(PluginInfo.ES_PLUGIN_POLICY); if (Files.exists(policyFile)) { - // parse the plugin's policy file into a set of permissions - Policy policy = Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toUri())); - PermissionCollection permissions = policy.getPermissions(Security.class.getProtectionDomain()); - // this method is supported with the specific implementation we use, but just check for safety. - if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) { - throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions"); - } - // grant the permissions to each jar in the plugin + // first get a list of URLs for the plugins' jars: + List codebases = new ArrayList<>(); try (DirectoryStream jarStream = Files.newDirectoryStream(plugin, "*.jar")) { for (Path jar : jarStream) { - if (map.put(jar.toUri().toURL().getFile(), permissions) != null) { - // just be paranoid ok? - throw new IllegalStateException("per-plugin permissions already granted for jar file: " + jar); - } + codebases.add(jar.toUri().toURL()); + } + } + + // parse the plugin's policy file into a set of permissions + Policy policy = readPolicy(policyFile.toUri().toURL(), codebases.toArray(new URL[codebases.size()])); + + // consult this policy for each of the plugin's jars: + for (URL url : codebases) { + if (map.put(url.getFile(), policy) != null) { + // just be paranoid ok? + throw new IllegalStateException("per-plugin permissions already granted for jar file: " + url); } } } @@ -194,6 +157,35 @@ final class Security { return Collections.unmodifiableMap(map); } + /** + * Reads and returns the specified {@code policyFile}. + *

+ * Resources (e.g. jar files and directories) listed in {@code codebases} location + * will be provided to the policy file via a system property of the short name: + * e.g. ${codebase.joda-convert-1.2.jar} would map to full URL. + */ + @SuppressForbidden(reason = "accesses fully qualified URLs to configure security") + static Policy readPolicy(URL policyFile, URL codebases[]) { + try { + try { + // set codebase properties + for (URL url : codebases) { + String shortName = PathUtils.get(url.toURI()).getFileName().toString(); + System.setProperty("codebase." + shortName, url.toString()); + } + return Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI())); + } finally { + // clear codebase properties + for (URL url : codebases) { + String shortName = PathUtils.get(url.toURI()).getFileName().toString(); + System.clearProperty("codebase." + shortName); + } + } + } catch (NoSuchAlgorithmException | URISyntaxException e) { + throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e); + } + } + /** returns dynamic Permissions to configured paths */ static Permissions createPermissions(Environment environment) throws IOException { Permissions policy = new Permissions(); diff --git a/core/src/main/resources/org/elasticsearch/bootstrap/security.policy b/core/src/main/resources/org/elasticsearch/bootstrap/security.policy index 7e7f347ce1b..244d5be6511 100644 --- a/core/src/main/resources/org/elasticsearch/bootstrap/security.policy +++ b/core/src/main/resources/org/elasticsearch/bootstrap/security.policy @@ -31,46 +31,12 @@ grant codeBase "file:${{java.ext.dirs}}/*" { //// Very special jar permissions: //// These are dangerous permissions that we don't want to grant to everything. -grant codeBase "${es.security.jar.lucene.core}" { +grant codeBase "${codebase.lucene-core-5.4.0-snapshot-1708254.jar}" { // needed to allow MMapDirectory's "unmap hack" permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; }; -//// test framework permissions. -//// These are mock objects and test management that we allow test framework libs -//// to provide on our behalf. But tests themselves cannot do this stuff! - -grant codeBase "${es.security.jar.elasticsearch.securemock}" { - // needed to access ReflectionFactory (see below) - permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect"; - // needed to support creation of mocks - permission java.lang.RuntimePermission "reflectionFactoryAccess"; - // needed for spy interception, etc - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; -}; - -grant codeBase "${es.security.jar.lucene.testframework}" { - // needed by RamUsageTester - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; -}; - -grant codeBase "${es.security.jar.randomizedtesting.runner}" { - // optionally needed for access to private test methods (e.g. beforeClass) - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; - - // needed for top threads handling - permission java.lang.RuntimePermission "modifyThreadGroup"; -}; - -grant codeBase "${es.security.jar.randomizedtesting.junit4}" { - // needed for gson serialization - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; - - // needed for stream redirection - permission java.lang.RuntimePermission "setIO"; -}; - //// Everything else: grant { @@ -126,10 +92,4 @@ grant { // needed by JDKESLoggerTests permission java.util.logging.LoggingPermission "control"; - - // needed to install SSLFactories, advanced SSL configuration, etc. - permission java.lang.RuntimePermission "setFactory"; - - // needed to allow installation of bouncycastle crypto provider - permission java.security.SecurityPermission "putProviderProperty.BC"; }; diff --git a/core/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy b/core/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy new file mode 100644 index 00000000000..f038c51c596 --- /dev/null +++ b/core/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//// additional test framework permissions. +//// These are mock objects and test management that we allow test framework libs +//// to provide on our behalf. But tests themselves cannot do this stuff! + +grant codeBase "${codebase.securemock-1.1.jar}" { + // needed to access ReflectionFactory (see below) + permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect"; + // needed to support creation of mocks + permission java.lang.RuntimePermission "reflectionFactoryAccess"; + // needed for spy interception, etc + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; +}; + +grant codeBase "${codebase.lucene-test-framework-5.4.0-snapshot-1708254.jar}" { + // needed by RamUsageTester + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; +}; + +grant codeBase "${codebase.randomizedtesting-runner-2.1.17.jar}" { + // optionally needed for access to private test methods (e.g. beforeClass) + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + + // needed for top threads handling + permission java.lang.RuntimePermission "modifyThreadGroup"; +}; + +grant codeBase "${codebase.junit4-ant-2.1.17.jar}" { + // needed for gson serialization + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + + // needed for stream redirection + permission java.lang.RuntimePermission "setIO"; +}; diff --git a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java index 3d19c5fb296..2c195a9a014 100644 --- a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java +++ b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java @@ -19,28 +19,37 @@ package org.elasticsearch.bootstrap; +import com.carrotsearch.randomizedtesting.RandomizedRunner; + +import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.TestSecurityManager; import org.elasticsearch.bootstrap.Bootstrap; import org.elasticsearch.bootstrap.ESPolicy; import org.elasticsearch.bootstrap.Security; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.plugins.PluginInfo; +import org.junit.Assert; import java.io.FilePermission; import java.io.InputStream; -import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.security.Permission; -import java.security.PermissionCollection; import java.security.Permissions; import java.security.Policy; -import java.security.URIParameter; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Properties; +import java.util.Set; import static com.carrotsearch.randomizedtesting.RandomizedTest.systemPropertyAsBoolean; @@ -83,7 +92,6 @@ public class BootstrapForTesting { // install security manager if requested if (systemPropertyAsBoolean("tests.security.manager", true)) { try { - Security.setCodebaseProperties(); // initialize paths the same exact way as bootstrap Permissions perms = new Permissions(); // add permissions to everything in classpath @@ -120,31 +128,17 @@ public class BootstrapForTesting { if (System.getProperty("tests.maven") == null) { perms.add(new RuntimePermission("setIO")); } - - final Policy policy; - // if its a plugin with special permissions, we use a wrapper policy impl to try - // to simulate what happens with a real distribution - List pluginPolicies = Collections.list(BootstrapForTesting.class.getClassLoader().getResources(PluginInfo.ES_PLUGIN_POLICY)); - if (!pluginPolicies.isEmpty()) { - Permissions extra = new Permissions(); - for (URL url : pluginPolicies) { - URI uri = url.toURI(); - Policy pluginPolicy = Policy.getInstance("JavaPolicy", new URIParameter(uri)); - PermissionCollection permissions = pluginPolicy.getPermissions(BootstrapForTesting.class.getProtectionDomain()); - // this method is supported with the specific implementation we use, but just check for safety. - if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) { - throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions"); - } - for (Permission permission : Collections.list(permissions.elements())) { - extra.add(permission); - } + + // read test-framework permissions + final Policy testFramework = Security.readPolicy(Bootstrap.class.getResource("test-framework.policy"), JarHell.parseClassPath()); + final Policy esPolicy = new ESPolicy(perms, getPluginPermissions()); + Policy.setPolicy(new Policy() { + @Override + public boolean implies(ProtectionDomain domain, Permission permission) { + // implements union + return esPolicy.implies(domain, permission) || testFramework.implies(domain, permission); } - // TODO: try to get rid of this class now that the world is simpler? - policy = new MockPluginPolicy(perms, extra); - } else { - policy = new ESPolicy(perms, Collections.emptyMap()); - } - Policy.setPolicy(policy); + }); System.setSecurityManager(new TestSecurityManager()); Security.selfTest(); @@ -168,6 +162,58 @@ public class BootstrapForTesting { } } + /** + * we dont know which codesources belong to which plugin, so just remove the permission from key codebases + * like core, test-framework, etc. this way tests fail if accesscontroller blocks are missing. + */ + @SuppressForbidden(reason = "accesses fully qualified URLs to configure security") + static Map getPluginPermissions() throws Exception { + List pluginPolicies = Collections.list(BootstrapForTesting.class.getClassLoader().getResources(PluginInfo.ES_PLUGIN_POLICY)); + if (pluginPolicies.isEmpty()) { + return Collections.emptyMap(); + } + + // compute classpath minus obvious places, all other jars will get the permission. + Set codebases = new HashSet<>(Arrays.asList(JarHell.parseClassPath())); + Set excluded = new HashSet<>(Arrays.asList( + // es core + Bootstrap.class.getProtectionDomain().getCodeSource().getLocation(), + // es test framework + BootstrapForTesting.class.getProtectionDomain().getCodeSource().getLocation(), + // lucene test framework + LuceneTestCase.class.getProtectionDomain().getCodeSource().getLocation(), + // randomized runner + RandomizedRunner.class.getProtectionDomain().getCodeSource().getLocation(), + // junit library + Assert.class.getProtectionDomain().getCodeSource().getLocation() + )); + codebases.removeAll(excluded); + + // parse each policy file, with codebase substitution from the classpath + final List policies = new ArrayList<>(); + for (URL policyFile : pluginPolicies) { + policies.add(Security.readPolicy(policyFile, codebases.toArray(new URL[codebases.size()]))); + } + + // consult each policy file for those codebases + Map map = new HashMap<>(); + for (URL url : codebases) { + map.put(url.getFile(), new Policy() { + @Override + public boolean implies(ProtectionDomain domain, Permission permission) { + // implements union + for (Policy p : policies) { + if (p.implies(domain, permission)) { + return true; + } + } + return false; + } + }); + } + return Collections.unmodifiableMap(map); + } + // does nothing, just easy way to make sure the class is loaded. public static void ensureInitialized() {} } diff --git a/core/src/test/java/org/elasticsearch/bootstrap/MockPluginPolicy.java b/core/src/test/java/org/elasticsearch/bootstrap/MockPluginPolicy.java deleted file mode 100644 index 91ed11cce63..00000000000 --- a/core/src/test/java/org/elasticsearch/bootstrap/MockPluginPolicy.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.bootstrap; - -import com.carrotsearch.randomizedtesting.RandomizedRunner; - -import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.common.logging.Loggers; -import org.junit.Assert; - -import java.net.URL; -import java.security.CodeSource; -import java.security.Permission; -import java.security.PermissionCollection; -import java.security.Policy; -import java.security.ProtectionDomain; -import java.security.cert.Certificate; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Simulates in unit tests per-plugin permissions. - * Unit tests for plugins do not have a proper plugin structure, - * so we don't know which codebases to apply the permission to. - *

- * As an approximation, we just exclude es/test/framework classes, - * because they will be present in stacks and fail tests for the - * simple case where an AccessController block is missing, because - * java security checks every codebase in the stacktrace, and we - * are sure to pollute it. - */ -final class MockPluginPolicy extends Policy { - final ESPolicy standardPolicy; - final PermissionCollection extraPermissions; - final Set excludedSources; - - /** - * Create a new MockPluginPolicy with dynamic {@code permissions} and - * adding the extra plugin permissions from {@code insecurePluginProp} to - * all code except test classes. - */ - MockPluginPolicy(PermissionCollection standard, PermissionCollection extra) throws Exception { - // the hack begins! - - this.standardPolicy = new ESPolicy(standard, Collections.emptyMap()); - this.extraPermissions = extra; - - excludedSources = new HashSet(); - // exclude some obvious places - // es core - excludedSources.add(Bootstrap.class.getProtectionDomain().getCodeSource()); - // es test framework - excludedSources.add(getClass().getProtectionDomain().getCodeSource()); - // lucene test framework - excludedSources.add(LuceneTestCase.class.getProtectionDomain().getCodeSource()); - // test runner - excludedSources.add(RandomizedRunner.class.getProtectionDomain().getCodeSource()); - // junit library - excludedSources.add(Assert.class.getProtectionDomain().getCodeSource()); - // scripts - excludedSources.add(new CodeSource(new URL("file:" + BootstrapInfo.UNTRUSTED_CODEBASE), (Certificate[])null)); - - Loggers.getLogger(getClass()).debug("Apply extra permissions [{}] excluding codebases [{}]", extraPermissions, excludedSources); - } - - @Override - public boolean implies(ProtectionDomain domain, Permission permission) { - CodeSource codeSource = domain.getCodeSource(); - // codesource can be null when reducing privileges via doPrivileged() - if (codeSource == null) { - return false; - } - - if (standardPolicy.implies(domain, permission)) { - return true; - } else if (excludedSources.contains(codeSource) == false && - codeSource.toString().contains("test-classes") == false) { - return extraPermissions.implies(permission); - } else { - return false; - } - } -} diff --git a/pom.xml b/pom.xml index afc9db6d632..c4292da93c2 100644 --- a/pom.xml +++ b/pom.xml @@ -45,10 +45,15 @@ -Xdoclint:-missing + + + 5.4.0 1708254 5.4.0-snapshot-${lucene.snapshot.revision} 2.1.17 + 1.1 + 2.6.2 1.6.2 1.2.17