security: Add _field_names field to the list of meta fields that are always allowed visible

The logic that filters `_field_names` field's terms is encapsulated in `FieldSubsetReader.java`,
but that doesn't kick in if `_field_names` is an allowed field.

Closes elastic/elasticsearch#2504

Original commit: elastic/x-pack-elasticsearch@d81ec9477a
This commit is contained in:
Martijn van Groningen 2016-06-13 15:44:55 +02:00
parent eb5248d127
commit 1ecebab0aa
3 changed files with 69 additions and 5 deletions

View File

@ -35,6 +35,7 @@ import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
import org.elasticsearch.index.engine.EngineException;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.internal.FieldNamesFieldMapper;
import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
import org.elasticsearch.index.query.ParsedQuery;
import org.elasticsearch.index.query.QueryBuilder;
@ -90,6 +91,7 @@ public class ShieldIndexSearcherWrapper extends IndexSearcherWrapper {
Set<String> allowedMetaFields = new HashSet<>();
allowedMetaFields.addAll(Arrays.asList(MapperService.getAllMetaFields()));
allowedMetaFields.add(FieldNamesFieldMapper.NAME); // TODO: add _field_names to MapperService#META_FIELDS?
allowedMetaFields.add("_source"); // TODO: add _source to MapperService#META_FIELDS?
allowedMetaFields.add("_version"); // TODO: add _version to MapperService#META_FIELDS?
allowedMetaFields.remove("_all"); // The _all field contains actual data and we can't include that by default.

View File

@ -34,6 +34,7 @@ import java.util.Collections;
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.existsQuery;
import static org.elasticsearch.index.query.QueryBuilders.hasChildQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
@ -881,28 +882,28 @@ public class FieldLevelSecurityTests extends ShieldIntegTestCase {
.prepareSearch("test")
.addSort("field1", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(1L));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(1L));
// user2 is not granted to use field1, so the default missing sort value is included
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field1", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
// user1 is not granted to use field2, so the default missing sort value is included
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field2", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
// user2 is granted to use field2, so it is included in the sort_values
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field2", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(2L));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(2L));
}
public void testAggs() throws Exception {
@ -1223,4 +1224,64 @@ public class FieldLevelSecurityTests extends ShieldIntegTestCase {
assertThat(response.getHits().getAt(0).sourceAsMap().get("field2").toString(), equalTo("value2"));
}
public void testExistQuery() {
assertAcked(client().admin().indices().prepareCreate("test")
.addMapping("type1", "field1", "type=text", "field2", "type=text", "field3", "type=text")
);
client().prepareIndex("test", "type1", "1").setSource("field1", "value1", "field2", "value2", "field3", "value3")
.setRefreshPolicy(IMMEDIATE)
.get();
// user1 has access to field1, so the query should match with the document:
SearchResponse response = client()
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 1);
// user1 has no access to field2, so the query should not match with the document:
response = client()
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 0);
// user2 has no access to field1, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 0);
// user2 has access to field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 1);
// user3 has access to field1 and field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user3", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 1);
// user3 has access to field1 and field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user3", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 1);
// user4 has access to no fields, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user4", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 0);
// user4 has access to no fields, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user4", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 0);
}
}

View File

@ -133,7 +133,7 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase {
FieldSubsetReader.FieldSubsetDirectoryReader result =
(FieldSubsetReader.FieldSubsetDirectoryReader) shieldIndexSearcherWrapper.wrap(esIn);
assertThat(result.getFieldNames().size(), equalTo(11));
assertThat(result.getFieldNames().size(), equalTo(12));
assertThat(result.getFieldNames().contains("_uid"), is(true));
assertThat(result.getFieldNames().contains("_id"), is(true));
assertThat(result.getFieldNames().contains("_version"), is(true));
@ -145,6 +145,7 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase {
assertThat(result.getFieldNames().contains("_ttl"), is(true));
assertThat(result.getFieldNames().contains("_size"), is(true));
assertThat(result.getFieldNames().contains("_index"), is(true));
assertThat(result.getFieldNames().contains("_field_names"), is(true));
// _all contains actual user data and therefor can't be included by default
assertThat(result.getFieldNames().contains("_all"), is(false));
}