Add a new async search security origin (#52141)

This commit adds a new security origin, and an associated reserved user
and role, named `_async_search`, which can be used by internal clients to
manage the `.async-search-*` restricted index namespace.
This commit is contained in:
Albert Zaharovits 2020-02-11 19:58:06 +02:00 committed by GitHub
parent d68a4ec82e
commit cc1fce96ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 174 additions and 16 deletions

View File

@ -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() {}

View File

@ -275,7 +275,8 @@ public class ReservedRolesStore implements BiConsumer<Set<String>, 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<String, Object> usageStats() {

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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));

View File

@ -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");

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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<Role> 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() {

View File

@ -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<String> 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<String> 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));
}
}
}

View File

@ -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<String> 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<String> 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<String> 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<String> 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() {