security: Added templating support to DLS' role query.

Closes elastic/elasticsearch#410

Original commit: elastic/x-pack-elasticsearch@2b91ea9eed
This commit is contained in:
Martijn van Groningen 2016-07-19 17:12:22 +02:00
parent d734d483c5
commit d33e639d4c
9 changed files with 607 additions and 11 deletions

View File

@ -0,0 +1,27 @@
apply plugin: 'elasticsearch.rest-test'
dependencies {
testCompile project(path: ':x-plugins:elasticsearch:x-pack', configuration: 'runtime')
testCompile project(path: ':modules:lang-mustache', configuration: 'runtime')
}
integTest {
cluster {
plugin ':x-plugins:elasticsearch:x-pack'
setting 'xpack.watcher.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
setting 'path.scripts', "${project.buildDir}/resources/test/templates"
setupCommand 'setupDummyUser',
'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'changeme', '-r', 'superuser'
waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}",
dest: tmpFile.toString(),
username: 'test_admin',
password: 'changeme',
ignoreerrors: true,
retries: 10)
return tmpFile.exists()
}
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.smoketest;
import com.carrotsearch.randomizedtesting.annotations.Name;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.test.rest.RestTestCandidate;
import org.elasticsearch.test.rest.parser.RestTestParseException;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import java.io.IOException;
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
public class RestIT extends ESRestTestCase {
private static final String BASIC_AUTH_VALUE = basicAuthHeaderValue("test_admin", new SecuredString("changeme".toCharArray()));
public RestIT(@Name("yaml") RestTestCandidate testCandidate) {
super(testCandidate);
}
@ParametersFactory
public static Iterable<Object[]> parameters() throws IOException, RestTestParseException {
return ESRestTestCase.createParameters(0, 1);
}
@Override
protected Settings restClientSettings() {
return Settings.builder()
.put(ThreadContext.PREFIX + ".Authorization", BASIC_AUTH_VALUE)
.build();
}
}

View File

@ -0,0 +1,191 @@
---
setup:
- skip:
features: headers
- do:
cluster.health:
wait_for_status: yellow
- do:
xpack.security.put_user:
username: "inline_template_user"
body: >
{
"password": "changeme",
"roles" : [ "inline_template_role" ]
}
- do:
xpack.security.put_user:
username: "stored_template_user"
body: >
{
"password": "changeme",
"roles" : [ "stored_template_role" ]
}
- do:
xpack.security.put_user:
username: "file_template_user"
body: >
{
"password": "changeme",
"roles" : [ "file_template_role" ]
}
- do:
xpack.security.put_role:
name: "inline_template_role"
body: >
{
"indices": [
{
"names": "foobar",
"privileges": ["all"],
"query" : {
"template" : {
"inline" : {
"term" : { "username" : "{{_user.username}}" }
}
}
}
}
]
}
- do:
xpack.security.put_role:
name: "stored_template_role"
body: >
{
"indices": [
{
"names": "foobar",
"privileges": ["all"],
"query" : {
"template" : {
"id" : "1"
}
}
}
]
}
- do:
xpack.security.put_role:
name: "file_template_role"
body: >
{
"indices": [
{
"names": "foobar",
"privileges": ["all"],
"query" : {
"template" : {
"file" : "query"
}
}
}
]
}
- do:
put_template:
id: "1"
body: >
{
"term" : {
"username" : "{{_user.username}}"
}
}
- do:
index:
index: foobar
type: type
id: 1
body: >
{
"username": "inline_template_user"
}
- do:
index:
index: foobar
type: type
id: 2
body: >
{
"username": "stored_template_user"
}
- do:
index:
index: foobar
type: type
id: 3
body: >
{
"username": "file_template_user"
}
- do:
indices.refresh: {}
---
teardown:
- do:
xpack.security.delete_user:
username: "inline_template_user"
ignore: 404
- do:
xpack.security.delete_user:
username: "stored_template_user"
ignore: 404
- do:
xpack.security.delete_user:
username: "file_template_user"
ignore: 404
- do:
xpack.security.delete_role:
name: "inline_template_role"
ignore: 404
- do:
xpack.security.delete_role:
name: "stored_template_role"
ignore: 404
- do:
xpack.security.delete_role:
name: "file_template_role"
ignore: 404
---
"Test inline template":
- do:
headers:
Authorization: "Basic aW5saW5lX3RlbXBsYXRlX3VzZXI6Y2hhbmdlbWU="
search:
index: foobar
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.username: inline_template_user}
---
"Test stored template":
- do:
headers:
Authorization: "Basic c3RvcmVkX3RlbXBsYXRlX3VzZXI6Y2hhbmdlbWU="
search:
index: foobar
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.username: stored_template_user}
---
"Test file template":
- do:
headers:
Authorization: "Basic ZmlsZV90ZW1wbGF0ZV91c2VyOmNoYW5nZW1l"
search:
index: foobar
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.username: file_template_user}

View File

@ -0,0 +1,198 @@
---
setup:
- skip:
features: headers
- do:
indices.create:
index: shared_logs
- do:
cluster.health:
wait_for_status: yellow
- do:
ingest.put_pipeline:
id: "my_pipeline"
body: >
{
"processors": [
{
"set_security_user" : {
"field" : "user"
}
}
]
}
- do:
xpack.security.put_user:
username: "joe"
body: >
{
"password": "changeme",
"roles" : [ "small_companies_role" ],
"metadata" : {
"customer_id" : "1"
}
}
- do:
xpack.security.put_user:
username: "john"
body: >
{
"password": "changeme",
"roles" : [ "small_companies_role" ],
"metadata" : {
"customer_id" : "2"
}
}
---
teardown:
- do:
xpack.security.delete_user:
username: "joe"
ignore: 404
- do:
xpack.security.delete_user:
username: "john"
ignore: 404
- do:
xpack.security.delete_role:
name: "small_companies_role"
ignore: 404
---
"Test shared index seperating user by using DLS role query with user's username":
- do:
xpack.security.put_role:
name: "small_companies_role"
body: >
{
"indices": [
{
"names": "shared_logs",
"privileges": ["read", "create"],
"query" : {
"template" : {
"inline" : {
"term" : { "user.username" : "{{_user.username}}" }
}
}
}
}
]
}
- do:
headers:
Authorization: "Basic am9lOmNoYW5nZW1l"
index:
index: shared_logs
type: type
pipeline: "my_pipeline"
body: >
{
"log": "Joe's first log entry"
}
- do:
headers:
Authorization: "Basic am9objpjaGFuZ2VtZQ=="
index:
index: shared_logs
type: type
pipeline: "my_pipeline"
body: >
{
"log": "John's first log entry"
}
- do:
indices.refresh: {}
# Joe searches:
- do:
headers:
Authorization: "Basic am9lOmNoYW5nZW1l"
search:
index: shared_logs
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.user.username: joe}
# John searches:
- do:
headers:
Authorization: "Basic am9objpjaGFuZ2VtZQ=="
search:
index: shared_logs
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.user.username: john}
---
"Test shared index seperating user by using DLS role query with user's metadata":
- do:
xpack.security.put_role:
name: "small_companies_role"
body: >
{
"indices": [
{
"names": "shared_logs",
"privileges": ["read", "create"],
"query" : {
"template" : {
"inline" : {
"term" : { "user.metadata.customer_id" : "{{_user.metadata.customer_id}}" }
}
}
}
}
]
}
- do:
headers:
Authorization: "Basic am9lOmNoYW5nZW1l"
index:
index: shared_logs
type: type
pipeline: "my_pipeline"
body: >
{
"log": "Joe's first log entry"
}
- do:
headers:
Authorization: "Basic am9objpjaGFuZ2VtZQ=="
index:
index: shared_logs
type: type
pipeline: "my_pipeline"
body: >
{
"log": "John's first log entry"
}
- do:
indices.refresh: {}
# Joe searches:
- do:
headers:
Authorization: "Basic am9lOmNoYW5nZW1l"
search:
index: shared_logs
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.user.username: joe}
# John searches:
- do:
headers:
Authorization: "Basic am9objpjaGFuZ2VtZQ=="
search:
index: shared_logs
body: { "query" : { "match_all" : {} } }
- match: { hits.total: 1}
- match: { hits.hits.0._source.user.username: john}

View File

@ -0,0 +1,5 @@
{
"term" : {
"username" : "{{_user.username}}"
}
}

View File

@ -430,7 +430,7 @@ public class Security implements ActionPlugin, IngestPlugin {
module.setSearcherWrapper((indexService) -> new SecurityIndexSearcherWrapper(indexService.getIndexSettings(), module.setSearcherWrapper((indexService) -> new SecurityIndexSearcherWrapper(indexService.getIndexSettings(),
indexService.newQueryShardContext(), indexService.mapperService(), indexService.newQueryShardContext(), indexService.mapperService(),
indexService.cache().bitsetFilterCache(), indexService.getIndexServices().getThreadPool().getThreadContext(), indexService.cache().bitsetFilterCache(), indexService.getIndexServices().getThreadPool().getThreadContext(),
securityLicenseState)); securityLicenseState, indexService.getIndexServices().getScriptService()));
} }
if (transportClientMode == false) { if (transportClientMode == false) {
/* We need to forcefully overwrite the query cache implementation to use security's opt out query cache implementation. /* We need to forcefully overwrite the query cache implementation to use security's opt out query cache implementation.

View File

@ -22,7 +22,9 @@ import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.BitSetIterator;
import org.apache.lucene.util.Bits; import org.apache.lucene.util.Bits;
import org.apache.lucene.util.SparseFixedBitSet; import org.apache.lucene.util.SparseFixedBitSet;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.logging.LoggerMessageFormat;
@ -43,16 +45,24 @@ import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.shard.IndexSearcherWrapper; import org.elasticsearch.index.shard.IndexSearcherWrapper;
import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.index.shard.ShardUtils; import org.elasticsearch.index.shard.ShardUtils;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.xpack.security.authc.Authentication;
import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationService;
import org.elasticsearch.xpack.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; import org.elasticsearch.xpack.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader;
import org.elasticsearch.xpack.security.SecurityLicenseState; import org.elasticsearch.xpack.security.SecurityLicenseState;
import org.elasticsearch.xpack.security.support.Exceptions; import org.elasticsearch.xpack.security.support.Exceptions;
import org.elasticsearch.xpack.security.user.User;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -78,10 +88,13 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper {
private final SecurityLicenseState securityLicenseState; private final SecurityLicenseState securityLicenseState;
private final ThreadContext threadContext; private final ThreadContext threadContext;
private final ESLogger logger; private final ESLogger logger;
private final ScriptService scriptService;
public SecurityIndexSearcherWrapper(IndexSettings indexSettings, QueryShardContext queryShardContext, public SecurityIndexSearcherWrapper(IndexSettings indexSettings, QueryShardContext queryShardContext,
MapperService mapperService, BitsetFilterCache bitsetFilterCache, MapperService mapperService, BitsetFilterCache bitsetFilterCache,
ThreadContext threadContext, SecurityLicenseState securityLicenseState) { ThreadContext threadContext, SecurityLicenseState securityLicenseState,
ScriptService scriptService) {
this.scriptService = scriptService;
this.logger = Loggers.getLogger(getClass(), indexSettings.getSettings()); this.logger = Loggers.getLogger(getClass(), indexSettings.getSettings());
this.mapperService = mapperService; this.mapperService = mapperService;
this.queryShardContext = queryShardContext; this.queryShardContext = queryShardContext;
@ -124,6 +137,7 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper {
BooleanQuery.Builder filter = new BooleanQuery.Builder(); BooleanQuery.Builder filter = new BooleanQuery.Builder();
for (BytesReference bytesReference : permissions.getQueries()) { for (BytesReference bytesReference : permissions.getQueries()) {
QueryShardContext queryShardContext = copyQueryShardContext(this.queryShardContext); QueryShardContext queryShardContext = copyQueryShardContext(this.queryShardContext);
bytesReference = evaluateTemplate(bytesReference);
try (XContentParser parser = XContentFactory.xContent(bytesReference).createParser(bytesReference)) { try (XContentParser parser = XContentFactory.xContent(bytesReference).createParser(bytesReference)) {
Optional<QueryBuilder> queryBuilder = queryShardContext.newParseContext(parser).parseInnerQueryBuilder(); Optional<QueryBuilder> queryBuilder = queryShardContext.newParseContext(parser).parseInnerQueryBuilder();
if (queryBuilder.isPresent()) { if (queryBuilder.isPresent()) {
@ -262,6 +276,45 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper {
} }
} }
BytesReference evaluateTemplate(BytesReference querySource) throws IOException {
try (XContentParser parser = XContentFactory.xContent(querySource).createParser(querySource)) {
XContentParser.Token token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("Unexpected token [" + token + "]");
}
token = parser.nextToken();
if (token != XContentParser.Token.FIELD_NAME) {
throw new ElasticsearchParseException("Unexpected token [" + token + "]");
}
if ("template".equals(parser.currentName())) {
token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("Unexpected token [" + token + "]");
}
Script script = Script.parse(parser, ParseFieldMatcher.EMPTY);
// Add the user details to the params
Map<String, Object> params = new HashMap<>();
if (script.getParams() != null) {
params.putAll(script.getParams());
}
User user = getUser();
Map<String, Object> userModel = new HashMap<>();
userModel.put("username", user.principal());
userModel.put("full_name", user.fullName());
userModel.put("email", user.email());
userModel.put("roles", Arrays.asList(user.roles()));
userModel.put("metadata", Collections.unmodifiableMap(user.metadata()));
params.put("_user", userModel);
// Always enforce mustache script lang:
script = new Script(script.getScript(), script.getType(), "mustache", params, script.getContentType());
ExecutableScript executable = scriptService.executable(script, ScriptContext.Standard.SEARCH, Collections.emptyMap());
return (BytesReference) executable.run();
} else {
return querySource;
}
}
}
protected IndicesAccessControl getIndicesAccessControl() { protected IndicesAccessControl getIndicesAccessControl() {
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationService.INDICES_PERMISSIONS_KEY); IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationService.INDICES_PERMISSIONS_KEY);
if (indicesAccessControl == null) { if (indicesAccessControl == null) {
@ -269,4 +322,9 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper {
} }
return indicesAccessControl; return indicesAccessControl;
} }
protected User getUser(){
Authentication authentication = Authentication.getAuthentication(threadContext);
return authentication.getUser();
}
} }

View File

@ -34,6 +34,7 @@ import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.xpack.security.SecurityLicenseState; import org.elasticsearch.xpack.security.SecurityLicenseState;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.IndexSettingsModule; import org.elasticsearch.test.IndexSettingsModule;
@ -55,13 +56,14 @@ public class SecurityIndexSearcherWrapperIntegrationTests extends ESTestCase {
public void testDLS() throws Exception { public void testDLS() throws Exception {
ShardId shardId = new ShardId("_index", "_na_", 0); ShardId shardId = new ShardId("_index", "_na_", 0);
MapperService mapperService = mock(MapperService.class); MapperService mapperService = mock(MapperService.class);
ScriptService scriptService = mock(ScriptService.class);
when(mapperService.docMappers(anyBoolean())).thenReturn(Collections.emptyList()); when(mapperService.docMappers(anyBoolean())).thenReturn(Collections.emptyList());
when(mapperService.simpleMatchToIndexNames(anyString())) when(mapperService.simpleMatchToIndexNames(anyString()))
.then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0])); .then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0]));
ThreadContext threadContext = new ThreadContext(Settings.EMPTY); ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, null, IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, null,
singleton(new BytesArray("{}"))); singleton(new BytesArray("{\"match_all\" : {}}")));
IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(shardId.getIndex(), Settings.EMPTY); IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(shardId.getIndex(), Settings.EMPTY);
QueryShardContext queryShardContext = mock(QueryShardContext.class); QueryShardContext queryShardContext = mock(QueryShardContext.class);
QueryParseContext queryParseContext = mock(QueryParseContext.class); QueryParseContext queryParseContext = mock(QueryParseContext.class);
@ -79,7 +81,7 @@ public class SecurityIndexSearcherWrapperIntegrationTests extends ESTestCase {
SecurityLicenseState licenseState = mock(SecurityLicenseState.class); SecurityLicenseState licenseState = mock(SecurityLicenseState.class);
when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(true); when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(true);
SecurityIndexSearcherWrapper wrapper = new SecurityIndexSearcherWrapper(indexSettings, queryShardContext, mapperService, SecurityIndexSearcherWrapper wrapper = new SecurityIndexSearcherWrapper(indexSettings, queryShardContext, mapperService,
bitsetFilterCache, threadContext, licenseState) { bitsetFilterCache, threadContext, licenseState, scriptService) {
@Override @Override
protected QueryShardContext copyQueryShardContext(QueryShardContext context) { protected QueryShardContext copyQueryShardContext(QueryShardContext context) {
@ -140,7 +142,7 @@ public class SecurityIndexSearcherWrapperIntegrationTests extends ESTestCase {
ParsedQuery parsedQuery = new ParsedQuery(new TermQuery(new Term("field", values[i]))); ParsedQuery parsedQuery = new ParsedQuery(new TermQuery(new Term("field", values[i])));
when(queryShardContext.newParseContext(any(XContentParser.class))).thenReturn(queryParseContext); when(queryShardContext.newParseContext(any(XContentParser.class))).thenReturn(queryParseContext);
when(queryParseContext.parseInnerQueryBuilder()) when(queryParseContext.parseInnerQueryBuilder())
.thenReturn(Optional.of((QueryBuilder) new TermQueryBuilder("field", values[i]))); .thenReturn(Optional.of(new TermQueryBuilder("field", values[i])));
when(queryShardContext.toQuery(any(QueryBuilder.class))).thenReturn(parsedQuery); when(queryShardContext.toQuery(any(QueryBuilder.class))).thenReturn(parsedQuery);
DirectoryReader wrappedDirectoryReader = wrapper.wrap(directoryReader); DirectoryReader wrappedDirectoryReader = wrapper.wrap(directoryReader);
IndexSearcher indexSearcher = wrapper.wrap(new IndexSearcher(wrappedDirectoryReader)); IndexSearcher indexSearcher = wrapper.wrap(new IndexSearcher(wrappedDirectoryReader));

View File

@ -35,33 +35,48 @@ import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.FixedBitSet; import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.SparseFixedBitSet; import org.apache.lucene.util.SparseFixedBitSet;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.env.Environment;
import org.elasticsearch.index.Index; import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.analysis.AnalysisService; import org.elasticsearch.index.analysis.AnalysisService;
import org.elasticsearch.index.cache.bitset.BitsetFilterCache; import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.internal.ParentFieldMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.index.similarity.SimilarityService;
import org.elasticsearch.indices.IndicesModule; import org.elasticsearch.indices.IndicesModule;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollector;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; import org.elasticsearch.xpack.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader;
import org.elasticsearch.xpack.security.SecurityLicenseState; import org.elasticsearch.xpack.security.SecurityLicenseState;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.IndexSettingsModule; import org.elasticsearch.test.IndexSettingsModule;
import org.elasticsearch.xpack.security.user.User;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.mockito.ArgumentCaptor;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
@ -75,13 +90,18 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.Matchers.sameInstance;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase { public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
private ThreadContext threadContext; private ThreadContext threadContext;
private MapperService mapperService; private MapperService mapperService;
private ScriptService scriptService;
private SecurityIndexSearcherWrapper securityIndexSearcherWrapper; private SecurityIndexSearcherWrapper securityIndexSearcherWrapper;
private ElasticsearchDirectoryReader esIn; private ElasticsearchDirectoryReader esIn;
private SecurityLicenseState licenseState; private SecurityLicenseState licenseState;
@ -90,6 +110,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
@Before @Before
public void before() throws Exception { public void before() throws Exception {
Index index = new Index("_index", "testUUID"); Index index = new Index("_index", "testUUID");
scriptService = mock(ScriptService.class);
indexSettings = IndexSettingsModule.newIndexSettings(index, Settings.EMPTY); indexSettings = IndexSettingsModule.newIndexSettings(index, Settings.EMPTY);
AnalysisService analysisService = new AnalysisService(indexSettings, Collections.emptyMap(), Collections.emptyMap(), AnalysisService analysisService = new AnalysisService(indexSettings, Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(), Collections.emptyMap()); Collections.emptyMap(), Collections.emptyMap());
@ -125,7 +146,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
mapperService.merge("type", new CompressedXContent(mappingSource.string()), MapperService.MergeReason.MAPPING_UPDATE, false); mapperService.merge("type", new CompressedXContent(mappingSource.string()), MapperService.MergeReason.MAPPING_UPDATE, false);
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState) { new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService) {
@Override @Override
protected IndicesAccessControl getIndicesAccessControl() { protected IndicesAccessControl getIndicesAccessControl() {
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true,
@ -156,14 +177,14 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
public void testWrapReaderWhenFeatureDisabled() throws Exception { public void testWrapReaderWhenFeatureDisabled() throws Exception {
when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(false); when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(false);
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState); new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService);
DirectoryReader reader = securityIndexSearcherWrapper.wrap(esIn); DirectoryReader reader = securityIndexSearcherWrapper.wrap(esIn);
assertThat(reader, sameInstance(esIn)); assertThat(reader, sameInstance(esIn));
} }
public void testWrapSearcherWhenFeatureDisabled() throws Exception { public void testWrapSearcherWhenFeatureDisabled() throws Exception {
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState); new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService);
IndexSearcher indexSearcher = new IndexSearcher(esIn); IndexSearcher indexSearcher = new IndexSearcher(esIn);
IndexSearcher result = securityIndexSearcherWrapper.wrap(indexSearcher); IndexSearcher result = securityIndexSearcherWrapper.wrap(indexSearcher);
assertThat(result, sameInstance(indexSearcher)); assertThat(result, sameInstance(indexSearcher));
@ -275,7 +296,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
DirectoryReader directoryReader = DocumentSubsetReader.wrap(esIn, bitsetFilterCache, new MatchAllDocsQuery()); DirectoryReader directoryReader = DocumentSubsetReader.wrap(esIn, bitsetFilterCache, new MatchAllDocsQuery());
IndexSearcher indexSearcher = new IndexSearcher(directoryReader); IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState); new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService);
IndexSearcher result = securityIndexSearcherWrapper.wrap(indexSearcher); IndexSearcher result = securityIndexSearcherWrapper.wrap(indexSearcher);
assertThat(result, not(sameInstance(indexSearcher))); assertThat(result, not(sameInstance(indexSearcher)));
assertThat(result.getSimilarity(true), sameInstance(indexSearcher.getSimilarity(true))); assertThat(result.getSimilarity(true), sameInstance(indexSearcher.getSimilarity(true)));
@ -284,7 +305,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
public void testIntersectScorerAndRoleBits() throws Exception { public void testIntersectScorerAndRoleBits() throws Exception {
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState); new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService);
final Directory directory = newDirectory(); final Directory directory = newDirectory();
IndexWriter iw = new IndexWriter( IndexWriter iw = new IndexWriter(
directory, directory,
@ -373,7 +394,7 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
private void assertResolvedFields(String expression, String... expectedFields) { private void assertResolvedFields(String expression, String... expectedFields) {
securityIndexSearcherWrapper = securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState) { new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService) {
@Override @Override
protected IndicesAccessControl getIndicesAccessControl() { protected IndicesAccessControl getIndicesAccessControl() {
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true,
@ -407,6 +428,60 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase {
doTestIndexSearcherWrapper(false, true); doTestIndexSearcherWrapper(false, true);
} }
public void testTemplating() throws Exception {
User user = new User("_username", new String[]{"role1", "role2"}, "_full_name", "_email",
Collections.singletonMap("key", "value"));
securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService) {
@Override
protected User getUser() {
return user;
}
};
ExecutableScript executableScript = mock(ExecutableScript.class);
when(scriptService.executable(any(Script.class), eq(ScriptContext.Standard.SEARCH), eq(Collections.emptyMap())))
.thenReturn(executableScript);
XContentBuilder builder = jsonBuilder();
String query = new TermQueryBuilder("field", "{{_user.username}}").toXContent(builder, ToXContent.EMPTY_PARAMS).string();
Script script = new Script(query, ScriptService.ScriptType.INLINE, null, Collections.singletonMap("custom", "value"));
builder = jsonBuilder().startObject().field("template");
script.toXContent(builder, ToXContent.EMPTY_PARAMS);
BytesReference querySource = builder.endObject().bytes();
securityIndexSearcherWrapper.evaluateTemplate(querySource);
ArgumentCaptor<Script> argument = ArgumentCaptor.forClass(Script.class);
verify(scriptService).executable(argument.capture(), eq(ScriptContext.Standard.SEARCH), eq(Collections.emptyMap()));
Script usedScript = argument.getValue();
assertThat(usedScript.getScript(), equalTo(script.getScript()));
assertThat(usedScript.getType(), equalTo(script.getType()));
assertThat(usedScript.getLang(), equalTo("mustache"));
assertThat(usedScript.getContentType(), equalTo(script.getContentType()));
assertThat(usedScript.getParams().size(), equalTo(2));
assertThat(usedScript.getParams().get("custom"), equalTo("value"));
Map<String, Object> userModel = new HashMap<>();
userModel.put("username", user.principal());
userModel.put("full_name", user.fullName());
userModel.put("email", user.email());
userModel.put("roles", Arrays.asList(user.roles()));
userModel.put("metadata", user.metadata());
assertThat(usedScript.getParams().get("_user"), equalTo(userModel));
}
public void testSkipTemplating() throws Exception {
securityIndexSearcherWrapper =
new SecurityIndexSearcherWrapper(indexSettings, null, mapperService, null, threadContext, licenseState, scriptService);
XContentBuilder builder = jsonBuilder();
BytesReference querySource = new TermQueryBuilder("field", "value").toXContent(builder, ToXContent.EMPTY_PARAMS).bytes();
BytesReference result = securityIndexSearcherWrapper.evaluateTemplate(querySource);
assertThat(result, sameInstance(querySource));
verifyZeroInteractions(scriptService);
}
static class CreateScorerOnceWeight extends Weight { static class CreateScorerOnceWeight extends Weight {
private final Weight weight; private final Weight weight;