diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 8917b7429f7..5c31ea03dcc 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -232,6 +232,9 @@ Changes in Runtime Behavior API, however the returned bits may be called on different documents compared to before. (Adrien Grand) +* LUCENE-6542: FSDirectory's ctor now works with security policies or file systems + that restrict write access. (Trejkaz, hossman, Uwe Schindler) + Optimizations * LUCENE-6548: Some optimizations for BlockTree's intersect with very @@ -264,6 +267,11 @@ Test Framework * LUCENE-6637: Fix FSTTester to not violate file permissions on -Dtests.verbose=true. (Mesbah M. Alam, Uwe Schindler) +* LUCENE-6542: LuceneTestCase now has runWithRestrictedPermissions() to run + an action with reduced permissions. This can be used to simulate special + environments (e.g., read-only dirs). If tests are running without a security + manager, an assume cancels test execution automatically. (Uwe Schindler) + Changes in Backwards Compatibility Policy * LUCENE-6553: The iterator returned by the LeafReader.postings method now diff --git a/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java b/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java index 629e467d924..e6bdfcbd49f 100644 --- a/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java +++ b/lucene/core/src/java/org/apache/lucene/store/FSDirectory.java @@ -125,7 +125,10 @@ public abstract class FSDirectory extends BaseDirectory { */ protected FSDirectory(Path path, LockFactory lockFactory) throws IOException { super(lockFactory); - Files.createDirectories(path); // create directory, if it doesn't exist + // If only read access is permitted, createDirectories fails even if the directory already exists. + if (!Files.isDirectory(path)) { + Files.createDirectories(path); // create directory, if it doesn't exist + } directory = path.toRealPath(); } diff --git a/lucene/core/src/test/org/apache/lucene/index/TestReadOnlyIndex.java b/lucene/core/src/test/org/apache/lucene/index/TestReadOnlyIndex.java new file mode 100644 index 00000000000..40b53b9172b --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/index/TestReadOnlyIndex.java @@ -0,0 +1,98 @@ +package org.apache.lucene.index; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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. + */ + +import java.io.FilePermission; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.PropertyPermission; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.MockAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.util.LuceneTestCase; +import org.junit.BeforeClass; + +public class TestReadOnlyIndex extends LuceneTestCase { + + private static final String longTerm = "longtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongtermlongterm"; + private static final String text = "This is the text to be indexed. " + longTerm; + + private static Path indexPath; + + @BeforeClass + public static void buildIndex() throws Exception { + indexPath = Files.createTempDirectory("readonlyindex"); + + // borrows from TestDemo, but not important to keep in sync with demo + Analyzer analyzer = new MockAnalyzer(random()); + Directory directory = newFSDirectory(indexPath); + RandomIndexWriter iwriter = new RandomIndexWriter(random(), directory, analyzer); + Document doc = new Document(); + doc.add(newTextField("fieldname", text, Field.Store.YES)); + iwriter.addDocument(doc); + iwriter.close(); + directory.close(); + analyzer.close(); + } + + public void testReadOnlyIndex() throws Exception { + runWithRestrictedPermissions(this::doTestReadOnlyIndex, + // add some basic permissions (because we are limited already - so we grant all important ones): + new RuntimePermission("*"), + new PropertyPermission("*", "read"), + // only allow read to the given index dir, nothing else: + new FilePermission(indexPath.toString(), "read"), + new FilePermission(indexPath.resolve("-").toString(), "read") + ); + } + + private Void doTestReadOnlyIndex() throws Exception { + Directory dir = FSDirectory.open(indexPath); + IndexReader ireader = DirectoryReader.open(dir); + IndexSearcher isearcher = newSearcher(ireader); + + // borrows from TestDemo, but not important to keep in sync with demo + + assertEquals(1, isearcher.search(new TermQuery(new Term("fieldname", longTerm)), 1).totalHits); + Query query = new TermQuery(new Term("fieldname", "text")); + TopDocs hits = isearcher.search(query, 1); + assertEquals(1, hits.totalHits); + // Iterate through the results: + for (int i = 0; i < hits.scoreDocs.length; i++) { + StoredDocument hitDoc = isearcher.doc(hits.scoreDocs[i].doc); + assertEquals(text, hitDoc.get("fieldname")); + } + + // Test simple phrase query + PhraseQuery phraseQuery = new PhraseQuery("fieldname", "to", "be"); + assertEquals(1, isearcher.search(phraseQuery, 1).totalHits); + + ireader.close(); + return null; // void + } + +} diff --git a/lucene/test-framework/src/java/org/apache/lucene/util/LuceneTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/util/LuceneTestCase.java index 2db1d6eeff1..1cc0684022e 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/util/LuceneTestCase.java +++ b/lucene/test-framework/src/java/org/apache/lucene/util/LuceneTestCase.java @@ -34,6 +34,14 @@ import java.lang.reflect.Method; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.ProtectionDomain; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; @@ -101,6 +109,7 @@ import org.junit.Test; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; import org.junit.runner.RunWith; + import com.carrotsearch.randomizedtesting.JUnit4MethodProvider; import com.carrotsearch.randomizedtesting.LifecycleScope; import com.carrotsearch.randomizedtesting.MixWithSuiteName; @@ -2604,6 +2613,25 @@ public abstract class LuceneTestCase extends Assert { public static Path createTempFile() throws IOException { return createTempFile("tempFile", ".tmp"); } + + /** + * Runs a code part with restricted permissions (be sure to add all required permissions, + * because it would start with empty permissions). You cannot grant more permissions than + * our policy file allows, but you may restrict writing to several dirs... + *

Note: This assumes a {@link SecurityManager} enabled, otherwise it + * stops test execution. + */ + public static T runWithRestrictedPermissions(PrivilegedExceptionAction action, Permission... permissions) throws Exception { + assumeTrue("runWithRestrictedPermissions requires a SecurityManager enabled", System.getSecurityManager() != null); + final PermissionCollection perms = new Permissions(); + Arrays.stream(permissions).forEach(perms::add); + final AccessControlContext ctx = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) }); + try { + return AccessController.doPrivileged(action, ctx); + } catch (PrivilegedActionException e) { + throw e.getException(); + } + } /** True if assertions (-ea) are enabled (at least for this class). */ public static final boolean assertsAreEnabled; diff --git a/lucene/test-framework/src/test/org/apache/lucene/util/TestRunWithRestrictedPermissions.java b/lucene/test-framework/src/test/org/apache/lucene/util/TestRunWithRestrictedPermissions.java new file mode 100644 index 00000000000..208b5c8d909 --- /dev/null +++ b/lucene/test-framework/src/test/org/apache/lucene/util/TestRunWithRestrictedPermissions.java @@ -0,0 +1,69 @@ +package org.apache.lucene.util; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.AllPermission; + +public class TestRunWithRestrictedPermissions extends LuceneTestCase { + + public void testDefaultsPass() throws Exception { + runWithRestrictedPermissions(this::doSomeForbiddenStuff, new AllPermission()); + } + + public void testNormallyAllowedStuff() throws Exception { + try { + runWithRestrictedPermissions(this::doSomeForbiddenStuff); + fail("this should not pass!"); + } catch (SecurityException se) { + // pass + } + } + + public void testCompletelyForbidden1() throws Exception { + try { + runWithRestrictedPermissions(this::doSomeCompletelyForbiddenStuff); + fail("this should not pass!"); + } catch (SecurityException se) { + // pass + } + } + + public void testCompletelyForbidden2() throws Exception { + try { + runWithRestrictedPermissions(this::doSomeCompletelyForbiddenStuff, new AllPermission()); + fail("this should not pass (not even with AllPermission)"); + } catch (SecurityException se) { + // pass + } + } + + private Void doSomeForbiddenStuff() throws IOException { + createTempDir("cannot_create_temp_folder"); + return null; // Void + } + + // something like this should not never pass!! + private Void doSomeCompletelyForbiddenStuff() throws IOException { + Files.createFile(Paths.get("denied")); + return null; // Void + } + +} diff --git a/lucene/tools/junit4/tests.policy b/lucene/tools/junit4/tests.policy index 797fec9cc89..35a966b2781 100644 --- a/lucene/tools/junit4/tests.policy +++ b/lucene/tools/junit4/tests.policy @@ -110,4 +110,7 @@ grant { // SSL related properties for jetty permission java.security.SecurityPermission "getProperty.ssl.KeyManagerFactory.algorithm"; permission java.security.SecurityPermission "getProperty.ssl.TrustManagerFactory.algorithm"; + + // allows LuceneTestCase#runWithRestrictedPermissions to execute with lower (or no) permission + permission java.security.SecurityPermission "createAccessControlContext"; };