diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java index 5fae2e99fb..94b4944015 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java @@ -38,11 +38,13 @@ import java.util.Map; import java.util.Set; import java.util.StringJoiner; +import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsRequest; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; @@ -103,6 +105,13 @@ public class PrivilegesEvaluator { private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); + + private static final Pattern DNFOF_PATTERNS = Pattern.compile( + "indices:(data/read/.*|(admin/(mappings/fields/get.*|shards/search_shards|resolve/index)))" + ); + + private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); + protected final Logger log = LogManager.getLogger(this.getClass()); private final ClusterService clusterService; @@ -386,10 +395,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } } - if (dnfofEnabled - && (action0.startsWith("indices:data/read/") - || action0.startsWith("indices:admin/mappings/fields/get") - || action0.equals("indices:admin/shards/search_shards"))) { + if (dnfofEnabled && DNFOF_PATTERNS.matcher(action0).matches()) { if(requestedResolved.getAllIndices().isEmpty()) { presponse.missingPrivileges.clear(); @@ -401,30 +407,21 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); if(reduced.isEmpty()) { - if(dcm.isDnfofForEmptyResultsEnabled()) { - if(request instanceof SearchRequest) { - ((SearchRequest) request).indices(new String[0]); - ((SearchRequest) request).indicesOptions(IndicesOptions.fromOptions(true, true, false, false)); - presponse.missingPrivileges.clear(); - presponse.allowed = true; - return presponse; - } + if(dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { - if(request instanceof ClusterSearchShardsRequest) { - ((ClusterSearchShardsRequest) request).indices(new String[0]); - ((ClusterSearchShardsRequest) request).indicesOptions(IndicesOptions.fromOptions(true, true, false, false)); - presponse.missingPrivileges.clear(); - presponse.allowed = true; - return presponse; - } + ((IndicesRequest.Replaceable) request).indices(new String[0]); + presponse.missingPrivileges.clear(); + presponse.allowed = true; - if(request instanceof GetFieldMappingsRequest) { - ((GetFieldMappingsRequest) request).indices(new String[0]); - ((GetFieldMappingsRequest) request).indicesOptions(IndicesOptions.fromOptions(true, true, false, false)); - presponse.missingPrivileges.clear(); - presponse.allowed = true; - return presponse; + if(request instanceof SearchRequest) { + ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); + } else if(request instanceof ClusterSearchShardsRequest) { + ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); + } else if(request instanceof GetFieldMappingsRequest) { + ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); } + + return presponse; } presponse.allowed = false; return presponse; diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java index 5c137ea490..e1f4595086 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java @@ -56,6 +56,7 @@ import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.delete.DeleteRequest; @@ -293,7 +294,9 @@ private void resolveTo(Iterable matchingAliases, Iterable matchi @Override public String[] provide(String[] original, Object localRequest, boolean supportsReplace) { final IndicesOptions indicesOptions = indicesOptionsFrom(localRequest); - final boolean enableCrossClusterResolution = localRequest instanceof FieldCapabilitiesRequest || localRequest instanceof SearchRequest; + final boolean enableCrossClusterResolution = localRequest instanceof FieldCapabilitiesRequest + || localRequest instanceof SearchRequest + || localRequest instanceof ResolveIndexAction.Request; // skip the whole thing if we have seen this exact resolveIndexPatterns request if (alreadyResolved.add(new MultiKey(indicesOptions, enableCrossClusterResolution, (original != null) ? new MultiKey(original, false) : null))) { diff --git a/src/main/resources/static_config/static_action_groups.yml b/src/main/resources/static_config/static_action_groups.yml index a863811726..a689e75884 100644 --- a/src/main/resources/static_config/static_action_groups.yml +++ b/src/main/resources/static_config/static_action_groups.yml @@ -42,6 +42,7 @@ search: allowed_actions: - "indices:data/read/search*" - "indices:data/read/msearch*" + - "indices:admin/resolve/index" - "suggest" type: "index" description: "Allow searching" @@ -87,6 +88,7 @@ read: allowed_actions: - "indices:data/read*" - "indices:admin/mappings/fields/get*" + - "indices:admin/resolve/index" type: "index" description: "Allow all read operations" indices_all: @@ -127,6 +129,7 @@ cluster_composite_ops_ro: - "indices:admin/aliases/exists*" - "indices:admin/aliases/get*" - "indices:data/read/scroll" + - "indices:admin/resolve/index" type: "cluster" description: "Allow readonly bulk and m* operations" get: diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/ResolveAPITests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/ResolveAPITests.java new file mode 100644 index 0000000000..4893226a0b --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/ResolveAPITests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package com.amazon.opendistroforelasticsearch.security; + +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; + +public class ResolveAPITests extends SingleClusterTest { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + @Test + public void testResolveDnfofFalse() throws Exception { + + Settings settings = Settings.builder().build(); + + setup(settings); + setupIndices(); + + final RestHelper rh = nonSslRestHelper(); + RestHelper.HttpResponse res; + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/*?pretty", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertContains(res, "*vulcangov*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*klingonempire*"); + assertContains(res, "*xyz*"); + assertContains(res, "*role01_role02*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/starfleet*?pretty", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*vulcangov*"); + assertNotContains(res, "*klingonempire*"); + assertNotContains(res, "*xyz*"); + assertNotContains(res, "*role01_role02*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*starfleet_academy*"); + assertContains(res, "*starfleet_library*"); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("_resolve/index/*?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + log.debug(res.getBody()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/starfleet*?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + log.debug(res.getBody()); + assertContains(res, "*starfleet*"); + assertContains(res, "*starfleet_academy*"); + assertContains(res, "*starfleet_library*"); + } + + @Test + public void testResolveDnfofTrue() throws Exception { + final Settings settings = Settings.builder().build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), settings); + setupIndices(); + + final RestHelper rh = nonSslRestHelper(); + RestHelper.HttpResponse res; + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/*?pretty", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertContains(res, "*vulcangov*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*klingonempire*"); + assertContains(res, "*xyz*"); + assertContains(res, "*role01_role02*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/starfleet*?pretty", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*vulcangov*"); + assertNotContains(res, "*klingonempire*"); + assertNotContains(res, "*xyz*"); + assertNotContains(res, "*role01_role02*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*starfleet_academy*"); + assertContains(res, "*starfleet_library*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/*?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*vulcangov*"); + assertNotContains(res, "*kirk*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*public*"); + assertContains(res, "*xyz*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_resolve/index/starfleet*?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + log.debug(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*vulcangov*"); + assertNotContains(res, "*kirk*"); + assertNotContains(res, "*public*"); + assertNotContains(res, "*xyz*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*starfleet_academy*"); + assertContains(res, "*starfleet_library*"); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("_resolve/index/vulcangov*?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + log.debug(res.getBody()); + } + + private void setupIndices() { + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("xyz").type("doc").setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(IndicesAliasesRequest.AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(IndicesAliasesRequest.AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(IndicesAliasesRequest.AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(IndicesAliasesRequest.AliasActions.add().indices("xyz").alias("alias1"))).actionGet(); + } + } +} \ No newline at end of file diff --git a/src/test/resources/action_groups.yml b/src/test/resources/action_groups.yml index dee9e2e8c5..45ef2134c1 100644 --- a/src/test/resources/action_groups.yml +++ b/src/test/resources/action_groups.yml @@ -76,6 +76,7 @@ OPENDISTRO_SECURITY_READ: hidden: false allowed_actions: - "indices:data/read*" + - "indices:admin/resolve/index" type: "index" description: "Migrated from v6" OPENDISTRO_SECURITY_DELETE: @@ -113,6 +114,7 @@ OPENDISTRO_SECURITY_GET: allowed_actions: - "indices:data/read/get*" - "indices:data/read/mget*" + - "indices:admin/resolve/index" type: "index" description: "Migrated from v6" OPENDISTRO_SECURITY_MANAGE: diff --git a/src/test/resources/action_groups_packaged.yml b/src/test/resources/action_groups_packaged.yml index 2c7a4feb63..662c7a19c3 100644 --- a/src/test/resources/action_groups_packaged.yml +++ b/src/test/resources/action_groups_packaged.yml @@ -77,6 +77,7 @@ OPENDISTRO_SECURITY_READ: allowed_actions: - "indices:data/read*" - "indices:admin/mappings/fields/get*" + - "indices:admin/resolve/index" type: "index" description: "Migrated from v6" OPENDISTRO_SECURITY_INDICES_ALL: