diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java index 61f2cbaccd0..ed03ab9ffc0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java @@ -52,6 +52,7 @@ public final class ClientHelper { public static final String ROLLUP_ORIGIN = "rollup"; public static final String ENRICH_ORIGIN = "enrich"; public static final String TRANSFORM_ORIGIN = "transform"; + public static final String ASYNC_SEARCH_ORIGIN = "async_search"; private ClientHelper() {} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 7899797b890..d8265601085 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -275,7 +275,8 @@ public class ReservedRolesStore implements BiConsumer, ActionListene } public static boolean isReserved(String role) { - return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || UsernamesField.XPACK_ROLE.equals(role); + return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || + UsernamesField.XPACK_ROLE.equals(role) || UsernamesField.ASYNC_SEARCH_ROLE.equals(role); } public Map usageStats() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java new file mode 100644 index 00000000000..db1df93cdf6 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java @@ -0,0 +1,53 @@ +/* + * 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.core.security.user; + +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; + +public class AsyncSearchUser extends User { + + public static final String NAME = UsernamesField.ASYNC_SEARCH_NAME; + public static final AsyncSearchUser INSTANCE = new AsyncSearchUser(); + public static final String ROLE_NAME = UsernamesField.ASYNC_SEARCH_ROLE; + public static final Role ROLE = Role.builder(new RoleDescriptor(ROLE_NAME, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + "*") + .privileges("all") + .allowRestrictedIndices(true).build(), + }, + null, + null, + null, + MetadataUtils.DEFAULT_RESERVED_METADATA, + null), null).build(); + + private AsyncSearchUser() { + super(NAME, ROLE_NAME); + } + + @Override + public boolean equals(Object o) { + return INSTANCE == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + public static boolean is(User user) { + return INSTANCE.equals(user); + } + + public static boolean is(String principal) { + return NAME.equals(principal); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java index 0a593ad9928..9c28b67a342 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java @@ -22,6 +22,8 @@ public final class UsernamesField { public static final String BEATS_ROLE = "beats_system"; public static final String APM_NAME = "apm_system"; public static final String APM_ROLE = "apm_system"; + public static final String ASYNC_SEARCH_NAME = "_async_search"; + public static final String ASYNC_SEARCH_ROLE = "_async_search"; public static final String REMOTE_MONITORING_NAME = "remote_monitoring_user"; public static final String REMOTE_MONITORING_COLLECTION_ROLE = "remote_monitoring_collector"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 11cc52a970f..100b8feec89 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -133,6 +133,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivileg import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; import org.elasticsearch.xpack.core.security.user.APMSystemUser; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; import org.elasticsearch.xpack.core.security.user.RemoteMonitoringUser; @@ -201,6 +202,7 @@ public class ReservedRolesStoreTests extends ESTestCase { assertThat(ReservedRolesStore.isReserved("kibana_dashboard_only_user"), is(true)); assertThat(ReservedRolesStore.isReserved("beats_admin"), is(true)); assertThat(ReservedRolesStore.isReserved(XPackUser.ROLE_NAME), is(true)); + assertThat(ReservedRolesStore.isReserved(AsyncSearchUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(LogstashSystemUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(BeatsSystemUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(APMSystemUser.ROLE_NAME), is(true)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index cd33ff10f0f..11c57fa3f5d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -12,6 +12,7 @@ import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.support.Automatons; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -19,6 +20,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.ENRICH_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.DEPRECATION_ORIGIN; @@ -116,6 +118,9 @@ public final class AuthorizationUtils { case TASKS_ORIGIN: // TODO use a more limited user for tasks securityContext.executeAsUser(XPackUser.INSTANCE, consumer, Version.CURRENT); break; + case ASYNC_SEARCH_ORIGIN: + securityContext.executeAsUser(AsyncSearchUser.INSTANCE, consumer, Version.CURRENT); + break; default: assert false : "action.origin [" + actionOrigin + "] is unknown!"; throw new IllegalStateException("action.origin [" + actionOrigin + "] should always be a known value"); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 54d86eda37f..7973fb42804 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -44,6 +44,7 @@ import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -212,6 +213,10 @@ public class CompositeRolesStore { roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE); return; } + if (AsyncSearchUser.is(user)) { + roleActionListener.onResponse(AsyncSearchUser.ROLE); + return; + } final Authentication.AuthenticationType authType = authentication.getAuthenticationType(); if (authType == Authentication.AuthenticationType.API_KEY) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java index f1767b11c78..5a0dcc1c1a9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java @@ -14,12 +14,14 @@ import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.junit.Before; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; @@ -95,9 +97,15 @@ public class AuthorizationUtilsTests extends ESTestCase { } public void testSwitchAndExecuteXpackUser() throws Exception { - String origin = randomFrom(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN, - ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN); - assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE); + for (String origin : Arrays.asList(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN, + ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN)) { + assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE); + } + } + + public void testSwitchAndExecuteAsyncSearchUser() throws Exception { + String origin = ClientHelper.ASYNC_SEARCH_ORIGIN; + assertSwitchBasedOnOriginAndExecute(origin, AsyncSearchUser.INSTANCE); } public void testSwitchWithTaskOrigin() throws Exception { @@ -124,10 +132,10 @@ public class AuthorizationUtilsTests extends ESTestCase { latch.countDown(); listener.onResponse(null); }; + threadContext.putHeader(headerName, headerValue); try (ThreadContext.StoredContext ignored = threadContext.stashWithOrigin(origin)) { AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer); - latch.await(); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 53d644eb9e4..84709843a5c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -56,6 +56,7 @@ import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -937,7 +938,7 @@ public class CompositeRolesStoreTests extends ESTestCase { assertThat(Arrays.asList(roles.names()), hasItem("anonymous_user_role")); } - public void testDoesNotUseRolesStoreForXPackUser() { + public void testDoesNotUseRolesStoreForXPacAndAsyncSearchUser() { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); @@ -959,13 +960,23 @@ public class CompositeRolesStoreTests extends ESTestCase { rds -> effectiveRoleDescriptors.set(rds)); verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + // test Xpack user short circuits to its own reserved role PlainActionFuture rolesFuture = new PlainActionFuture<>(); Authentication auth = new Authentication(XPackUser.INSTANCE, new RealmRef("name", "type", "node"), null); compositeRolesStore.getRoles(XPackUser.INSTANCE, auth, rolesFuture); - final Role roles = rolesFuture.actionGet(); + Role roles = rolesFuture.actionGet(); assertThat(roles, equalTo(XPackUser.ROLE)); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); verifyNoMoreInteractions(fileRolesStore, nativeRolesStore, reservedRolesStore); + + // test AyncSearch user short circuits to its own reserved role + rolesFuture = new PlainActionFuture<>(); + auth = new Authentication(AsyncSearchUser.INSTANCE, new RealmRef("name", "type", "node"), null); + compositeRolesStore.getRoles(AsyncSearchUser.INSTANCE, auth, rolesFuture); + roles = rolesFuture.actionGet(); + assertThat(roles, equalTo(AsyncSearchUser.ROLE)); + assertThat(effectiveRoleDescriptors.get(), is(nullValue())); + verifyNoMoreInteractions(fileRolesStore, nativeRolesStore, reservedRolesStore); } public void testGetRolesForSystemUserThrowsException() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AsyncSearchUserTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AsyncSearchUserTests.java new file mode 100644 index 00000000000..38722cf8a42 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AsyncSearchUserTests.java @@ -0,0 +1,60 @@ +/* + * 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.user; + +import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchAction; +import org.hamcrest.Matchers; + +import java.util.Arrays; +import java.util.function.Predicate; + +import static org.mockito.Mockito.mock; + +public class AsyncSearchUserTests extends ESTestCase { + + public void testAsyncSearchUserCannotAccessNonRestrictedIndices() { + for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { + Predicate predicate = AsyncSearchUser.ROLE.indices().allowedIndicesMatcher(action); + String index = randomAlphaOfLengthBetween(3, 12); + if (false == RestrictedIndicesNames.isRestricted(index)) { + assertThat(predicate.test(index), Matchers.is(false)); + } + index = "." + randomAlphaOfLengthBetween(3, 12); + if (false == RestrictedIndicesNames.isRestricted(index)) { + assertThat(predicate.test(index), Matchers.is(false)); + } + } + } + + public void testAsyncSearchUserCanAccessOnlyAsyncSearchRestrictedIndices() { + for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { + final Predicate predicate = AsyncSearchUser.ROLE.indices().allowedIndicesMatcher(action); + for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) { + assertThat(predicate.test(index), Matchers.is(false)); + } + assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 3)), Matchers.is(true)); + } + } + + public void testAsyncSearchUserHasNoClusterPrivileges() { + for (String action : Arrays.asList(ClusterStateAction.NAME, GetWatchAction.NAME, ClusterStatsAction.NAME, NodesStatsAction.NAME)) { + assertThat(AsyncSearchUser.ROLE.cluster().check(action, mock(TransportRequest.class), mock(Authentication.class)), + Matchers.is(false)); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/XPackUserTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/XPackUserTests.java index 6c7fb2abdab..dc34ffad8d6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/XPackUserTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/XPackUserTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.user; +import org.elasticsearch.action.delete.DeleteAction; import org.elasticsearch.action.get.GetAction; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.search.SearchAction; @@ -17,24 +18,33 @@ import org.elasticsearch.xpack.security.audit.index.IndexNameResolver; import org.hamcrest.Matchers; import org.joda.time.DateTime; +import java.util.Arrays; import java.util.function.Predicate; public class XPackUserTests extends ESTestCase { public void testXPackUserCanAccessNonSecurityIndices() { - final String action = randomFrom(GetAction.NAME, SearchAction.NAME, IndexAction.NAME); - final Predicate predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action); - final String index = randomBoolean() ? randomAlphaOfLengthBetween(3, 12) : "." + randomAlphaOfLength(8); - assertThat(predicate.test(index), Matchers.is(true)); + for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { + Predicate predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action); + String index = randomAlphaOfLengthBetween(3, 12); + if (false == RestrictedIndicesNames.isRestricted(index)) { + assertThat(predicate.test(index), Matchers.is(true)); + } + index = "." + randomAlphaOfLengthBetween(3, 12); + if (false == RestrictedIndicesNames.isRestricted(index)) { + assertThat(predicate.test(index), Matchers.is(true)); + } + } } public void testXPackUserCannotAccessRestrictedIndices() { - final String action = randomFrom(GetAction.NAME, SearchAction.NAME, IndexAction.NAME); - final Predicate predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action); - for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) { - assertThat(predicate.test(index), Matchers.is(false)); + for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { + Predicate predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action); + for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) { + assertThat(predicate.test(index), Matchers.is(false)); + } + assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)), Matchers.is(false)); } - assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)), Matchers.is(false)); } public void testXPackUserCanReadAuditTrail() {