From 652f6560b81e2d5083eadf6b68f5f3267c70ffe3 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 10 Oct 2017 10:00:34 +0200 Subject: [PATCH] security: Always allow access to a rootdoc's nested documents if access to rootdoc is allowed relates elastic/x-pack-elasticsearch#2665 Original commit: elastic/x-pack-elasticsearch@2bbddd1dd2bd89c74bff08ddbd1bccd948383393 --- .../SecurityIndexSearcherWrapper.java | 23 +++++---- .../DocumentLevelSecurityTests.java | 47 +++++++++++++++++-- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java index be42573bd3a..3f47c5a360a 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java @@ -17,24 +17,21 @@ import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.LeafCollector; +import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Weight; +import org.apache.lucene.search.join.BitSetProducer; +import org.apache.lucene.search.join.ToChildBlockJoinQuery; import org.apache.lucene.util.BitSet; import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.SparseFixedBitSet; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionRequestBuilder; -import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.client.Client; -import org.elasticsearch.client.FilterClient; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentFactory; @@ -46,7 +43,6 @@ import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoostingQueryBuilder; import org.elasticsearch.index.query.ConstantScoreQueryBuilder; import org.elasticsearch.index.query.GeoShapeQueryBuilder; -import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.QueryShardContext; @@ -139,10 +135,17 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper { QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser); verifyRoleQuery(queryBuilder); failIfQueryUsesClient(queryBuilder, queryShardContext); - ParsedQuery parsedQuery = queryShardContext.toFilter(queryBuilder); - filter.add(parsedQuery.query(), SHOULD); + Query roleQuery = queryShardContext.toFilter(queryBuilder).query(); + filter.add(roleQuery, SHOULD); + if (queryShardContext.getMapperService().hasNested()) { + // If access is allowed on root doc then also access is allowed on all nested docs of that root document: + BitSetProducer rootDocs = queryShardContext.bitsetFilter(Queries.newNonNestedFilter()); + ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs); + filter.add(includeNestedDocs, SHOULD); + } } } + // at least one of the queries should match filter.setMinimumNumberShouldMatch(1); reader = DocumentSubsetReader.wrap(reader, bitsetFilterCache, new ConstantScoreQuery(filter.build())); diff --git a/plugin/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java b/plugin/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java index 96cd8e250a1..ef307bd9d92 100644 --- a/plugin/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java +++ b/plugin/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java @@ -23,6 +23,8 @@ import org.elasticsearch.client.Requests; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.InnerHitBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.indices.IndicesRequestCache; @@ -37,8 +39,6 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortMode; import org.elasticsearch.search.sort.SortOrder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.test.InternalSettingsPlugin; import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.xpack.XPackPlugin; @@ -52,6 +52,7 @@ import java.util.HashMap; import java.util.Map; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery; @@ -658,7 +659,7 @@ public class DocumentLevelSecurityTests extends SecurityIntegTestCase { } public void testParentChild_joinField() throws Exception { - XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() + XContentBuilder mapping = jsonBuilder().startObject() .startObject("properties") .startObject("join_field") .field("type", "join") @@ -903,4 +904,44 @@ public class DocumentLevelSecurityTests extends SecurityIntegTestCase { assertThat(client().prepareGet("test", "type", "1").get().getSource().get("field1").toString(), equalTo("value3")); } + public void testNestedInnerHits() throws Exception { + assertAcked(client().admin().indices().prepareCreate("test") + .addMapping("type1", "field1", "type=text", "nested_field", "type=nested") + ); + client().prepareIndex("test", "type1", "1") + .setSource(jsonBuilder().startObject() + .field("field1", "value1") + .startArray("nested_field") + .startObject() + .field("field2", "value2") + .endObject() + .endArray() + .endObject()) + .get(); + client().prepareIndex("test", "type1", "2") + .setSource(jsonBuilder().startObject() + .field("field1", "value2") + .startArray("nested_field") + .startObject() + .field("field2", "value2") + .endObject() + .endArray() + .endObject()) + .get(); + refresh("test"); + + SearchResponse response = client() + .filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD))) + .prepareSearch("test") + .setQuery(QueryBuilders.nestedQuery("nested_field", QueryBuilders.termQuery("nested_field.field2", "value2"), + ScoreMode.None).innerHit(new InnerHitBuilder())) + .get(); + assertHitCount(response, 1); + assertSearchHits(response, "1"); + assertThat(response.getHits().getAt(0).getInnerHits().get("nested_field").getAt(0).getId(), equalTo("1")); + assertThat(response.getHits().getAt(0).getInnerHits().get("nested_field").getAt(0).getNestedIdentity().getOffset(), equalTo(0)); + assertThat(response.getHits().getAt(0).getInnerHits().get("nested_field").getAt(0).getSourceAsString(), + equalTo("{\"nested_field\":{\"field2\":\"value2\"}}")); + } + }