diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractor.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractor.java new file mode 100644 index 00000000000..bf5ab116a83 --- /dev/null +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractor.java @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authz.accesscontrol; + +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.DisjunctionMaxQuery; +import org.apache.lucene.search.DocValuesNumbersQuery; +import org.apache.lucene.search.DocValuesRangeQuery; +import org.apache.lucene.search.FieldValueQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PointInSetQuery; +import org.apache.lucene.search.PointRangeQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.SynonymQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.spans.SpanTermQuery; + +import java.util.Set; + +/** + * Extracts fields from a query, or throws UnsupportedOperationException. + *

+ * Lucene queries have {@link Weight#extractTerms}, but this is really geared at things + * such as highlighting, not security. For example terms in a Boolean {@code MUST_NOT} clause + * are not included, TermsQuery doesn't implement the method as it could be terribly slow, etc. + */ +class FieldExtractor { + + /** + * Populates {@code fields} with the set of fields used by the query, or throws + * UnsupportedOperationException if it doesn't know how to do this. + */ + static void extractFields(Query query, Set fields) throws UnsupportedOperationException { + // NOTE: we expect a rewritten query, so we only need logic for "atomic" queries here: + if (query instanceof BooleanQuery) { + // extract from all clauses + BooleanQuery q = (BooleanQuery) query; + for (BooleanClause clause : q.clauses()) { + extractFields(clause.getQuery(), fields); + } + } else if (query instanceof DisjunctionMaxQuery) { + // extract from all clauses + DisjunctionMaxQuery q = (DisjunctionMaxQuery) query; + for (Query clause : q.getDisjuncts()) { + extractFields(clause, fields); + } + } else if (query instanceof SpanTermQuery) { + // we just do SpanTerm, other spans are trickier, they could contain + // the evil FieldMaskingSpanQuery: so SpanQuery.getField cannot be trusted. + fields.add(((SpanTermQuery)query).getField()); + } else if (query instanceof TermQuery) { + fields.add(((TermQuery)query).getTerm().field()); + } else if (query instanceof SynonymQuery) { + SynonymQuery q = (SynonymQuery) query; + // all terms must have the same field + fields.add(q.getTerms().get(0).field()); + } else if (query instanceof PhraseQuery) { + PhraseQuery q = (PhraseQuery) query; + // all terms must have the same field + fields.add(q.getTerms()[0].field()); + } else if (query instanceof MultiPhraseQuery) { + MultiPhraseQuery q = (MultiPhraseQuery) query; + // all terms must have the same field + fields.add(q.getTermArrays()[0][0].field()); + } else if (query instanceof PointRangeQuery) { + fields.add(((PointRangeQuery)query).getField()); + } else if (query instanceof PointInSetQuery) { + fields.add(((PointInSetQuery)query).getField()); + } else if (query instanceof FieldValueQuery) { + fields.add(((FieldValueQuery)query).getField()); + } else if (query instanceof DocValuesNumbersQuery) { + fields.add(((DocValuesNumbersQuery)query).getField()); + } else if (query instanceof DocValuesRangeQuery) { + fields.add(((DocValuesRangeQuery)query).getField()); + } else if (query instanceof MatchAllDocsQuery) { + // no field + } else if (query instanceof MatchNoDocsQuery) { + // no field + } else { + throw new UnsupportedOperationException(); // we don't know how to get the fields from it + } + } +} diff --git a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java index c95d44096c8..7cf8a0e0e83 100644 --- a/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java +++ b/elasticsearch/x-pack/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java @@ -16,8 +16,12 @@ import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.search.internal.ShardSearchRequest; import org.elasticsearch.xpack.security.authz.InternalAuthorizationService; +import java.util.HashSet; +import java.util.Set; + /** - * Opts out of the query cache if field level security is active for the current request. + * Opts out of the query cache if field level security is active for the current request, + * and its unsafe to cache. */ public final class OptOutQueryCache extends AbstractIndexComponent implements QueryCache { @@ -64,13 +68,41 @@ public final class OptOutQueryCache extends AbstractIndexComponent implements Qu IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(indexName); if (indexAccessControl != null && indexAccessControl.getFields() != null) { - logger.debug("opting out of the query cache. request for index [{}] has field level security enabled", indexName); - // If in the future there is a Query#extractFields() then we can be smart on when to skip the query cache. - // (only cache if all fields in the query also are defined in the role) - return weight; + if (cachingIsSafe(weight, indexAccessControl)) { + logger.trace("not opting out of the query cache. request for index [{}] is safe to cache", indexName); + return indicesQueryCache.doCache(weight, policy); + } else { + logger.trace("opting out of the query cache. request for index [{}] is unsafe to cache", indexName); + return weight; + } } else { logger.trace("not opting out of the query cache. request for index [{}] has field level security disabled", indexName); return indicesQueryCache.doCache(weight, policy); } } + + /** + * Returns true if its safe to use the query cache for this query. + */ + static boolean cachingIsSafe(Weight weight, IndicesAccessControl.IndexAccessControl permissions) { + // support caching for common queries, by inspecting the field + // TODO: If in the future there is a Query#extractFields() then we can do a better job + Set fields = new HashSet<>(); + try { + FieldExtractor.extractFields(weight.getQuery(), fields); + } catch (UnsupportedOperationException ok) { + // we don't know how to safely extract the fields of this query, don't cache. + return false; + } + + // we successfully extracted the set of fields: check each one + for (String field : fields) { + // don't cache any internal fields (e.g. _field_names), these are complicated. + if (field.startsWith("_") || permissions.getFields().contains(field) == false) { + return false; + } + } + // we can cache, all fields are ok + return true; + } } diff --git a/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractorTests.java b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractorTests.java new file mode 100644 index 00000000000..b0e42a0d121 --- /dev/null +++ b/elasticsearch/x-pack/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/FieldExtractorTests.java @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authz.accesscontrol; + +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.AssertingQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.DisjunctionMaxQuery; +import org.apache.lucene.search.DocValuesNumbersQuery; +import org.apache.lucene.search.DocValuesRangeQuery; +import org.apache.lucene.search.FieldValueQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.SynonymQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.spans.SpanTermQuery; +import org.elasticsearch.test.ESTestCase; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** Simple tests for query field extraction */ +public class FieldExtractorTests extends ESTestCase { + + public void testBoolean() { + Set fields = new HashSet<>(); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new TermQuery(new Term("foo", "bar")), BooleanClause.Occur.MUST); + builder.add(new TermQuery(new Term("no", "baz")), BooleanClause.Occur.MUST_NOT); + FieldExtractor.extractFields(builder.build(), fields); + assertEquals(asSet("foo", "no"), fields); + } + + public void testDisjunctionMax() { + Set fields = new HashSet<>(); + DisjunctionMaxQuery query = new DisjunctionMaxQuery(Arrays.asList( + new TermQuery(new Term("one", "bar")), + new TermQuery(new Term("two", "baz")) + ), 1.0F); + FieldExtractor.extractFields(query, fields); + assertEquals(asSet("one", "two"), fields); + } + + public void testSpanTerm() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new SpanTermQuery(new Term("foo", "bar")), fields); + assertEquals(asSet("foo"), fields); + } + + public void testTerm() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new TermQuery(new Term("foo", "bar")), fields); + assertEquals(asSet("foo"), fields); + } + + public void testSynonym() { + Set fields = new HashSet<>(); + SynonymQuery query = new SynonymQuery(new Term("foo", "bar"), new Term("foo", "baz")); + FieldExtractor.extractFields(query, fields); + assertEquals(asSet("foo"), fields); + } + + public void testPhrase() { + Set fields = new HashSet<>(); + PhraseQuery.Builder builder = new PhraseQuery.Builder(); + builder.add(new Term("foo", "bar")); + builder.add(new Term("foo", "baz")); + FieldExtractor.extractFields(builder.build(), fields); + assertEquals(asSet("foo"), fields); + } + + public void testMultiPhrase() { + Set fields = new HashSet<>(); + MultiPhraseQuery.Builder builder = new MultiPhraseQuery.Builder(); + builder.add(new Term("foo", "bar")); + builder.add(new Term[] { new Term("foo", "baz"), new Term("foo", "baz2") }); + FieldExtractor.extractFields(builder.build(), fields); + assertEquals(asSet("foo"), fields); + } + + public void testPointRange() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(IntPoint.newRangeQuery("foo", 3, 4), fields); + assertEquals(asSet("foo"), fields); + } + + public void testPointSet() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(IntPoint.newSetQuery("foo", 3, 4, 5), fields); + assertEquals(asSet("foo"), fields); + } + + public void testFieldValue() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new FieldValueQuery("foo"), fields); + assertEquals(asSet("foo"), fields); + } + + public void testDocValuesNumbers() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new DocValuesNumbersQuery("foo", 5L), fields); + assertEquals(asSet("foo"), fields); + } + + public void testDocValuesRange() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(DocValuesRangeQuery.newLongRange("foo", 1L, 2L, true, true), fields); + assertEquals(asSet("foo"), fields); + } + + public void testMatchAllDocs() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new MatchAllDocsQuery(), fields); + assertEquals(Collections.emptySet(), fields); + } + + public void testMatchNoDocs() { + Set fields = new HashSet<>(); + FieldExtractor.extractFields(new MatchNoDocsQuery(), fields); + assertEquals(Collections.emptySet(), fields); + } + + public void testUnsupported() { + Set fields = new HashSet<>(); + expectThrows(UnsupportedOperationException.class, () -> { + FieldExtractor.extractFields(new AssertingQuery(random(), new MatchAllDocsQuery()), fields); + }); + } +}