Browse Source

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.
Albert Zaharovits 5 years ago
parent
commit
97f6e972b9

+ 1 - 0
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() {}
 

+ 2 - 1
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<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() {

+ 53 - 0
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);
+    }
+}

+ 2 - 0
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";

+ 2 - 0
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));

+ 5 - 0
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");

+ 5 - 0
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) {

+ 12 - 4
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();
         }
     }

+ 13 - 2
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<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() {

+ 60 - 0
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<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));
+        }
+    }
+}

+ 19 - 9
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<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() {