Skip to content

Commit 97f6e97

Browse files
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.
1 parent 4a3ca41 commit 97f6e97

File tree

11 files changed

+174
-16
lines changed

11 files changed

+174
-16
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public final class ClientHelper {
5252
public static final String ROLLUP_ORIGIN = "rollup";
5353
public static final String ENRICH_ORIGIN = "enrich";
5454
public static final String TRANSFORM_ORIGIN = "transform";
55+
public static final String ASYNC_SEARCH_ORIGIN = "async_search";
5556

5657
private ClientHelper() {}
5758

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ private static RoleDescriptor kibanaAdminUser(String name, Map<String, Object> m
275275
}
276276

277277
public static boolean isReserved(String role) {
278-
return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || UsernamesField.XPACK_ROLE.equals(role);
278+
return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) ||
279+
UsernamesField.XPACK_ROLE.equals(role) || UsernamesField.ASYNC_SEARCH_ROLE.equals(role);
279280
}
280281

281282
public Map<String, Object> usageStats() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.user;
7+
8+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
9+
import org.elasticsearch.xpack.core.security.authz.permission.Role;
10+
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
11+
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
12+
13+
public class AsyncSearchUser extends User {
14+
15+
public static final String NAME = UsernamesField.ASYNC_SEARCH_NAME;
16+
public static final AsyncSearchUser INSTANCE = new AsyncSearchUser();
17+
public static final String ROLE_NAME = UsernamesField.ASYNC_SEARCH_ROLE;
18+
public static final Role ROLE = Role.builder(new RoleDescriptor(ROLE_NAME,
19+
null,
20+
new RoleDescriptor.IndicesPrivileges[] {
21+
RoleDescriptor.IndicesPrivileges.builder()
22+
.indices(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + "*")
23+
.privileges("all")
24+
.allowRestrictedIndices(true).build(),
25+
},
26+
null,
27+
null,
28+
null,
29+
MetadataUtils.DEFAULT_RESERVED_METADATA,
30+
null), null).build();
31+
32+
private AsyncSearchUser() {
33+
super(NAME, ROLE_NAME);
34+
}
35+
36+
@Override
37+
public boolean equals(Object o) {
38+
return INSTANCE == o;
39+
}
40+
41+
@Override
42+
public int hashCode() {
43+
return System.identityHashCode(this);
44+
}
45+
46+
public static boolean is(User user) {
47+
return INSTANCE.equals(user);
48+
}
49+
50+
public static boolean is(String principal) {
51+
return NAME.equals(principal);
52+
}
53+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public final class UsernamesField {
2222
public static final String BEATS_ROLE = "beats_system";
2323
public static final String APM_NAME = "apm_system";
2424
public static final String APM_ROLE = "apm_system";
25+
public static final String ASYNC_SEARCH_NAME = "_async_search";
26+
public static final String ASYNC_SEARCH_ROLE = "_async_search";
2527

2628
public static final String REMOTE_MONITORING_NAME = "remote_monitoring_user";
2729
public static final String REMOTE_MONITORING_COLLECTION_ROLE = "remote_monitoring_collector";

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
134134
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
135135
import org.elasticsearch.xpack.core.security.user.APMSystemUser;
136+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
136137
import org.elasticsearch.xpack.core.security.user.BeatsSystemUser;
137138
import org.elasticsearch.xpack.core.security.user.LogstashSystemUser;
138139
import org.elasticsearch.xpack.core.security.user.RemoteMonitoringUser;
@@ -201,6 +202,7 @@ public void testIsReserved() {
201202
assertThat(ReservedRolesStore.isReserved("kibana_dashboard_only_user"), is(true));
202203
assertThat(ReservedRolesStore.isReserved("beats_admin"), is(true));
203204
assertThat(ReservedRolesStore.isReserved(XPackUser.ROLE_NAME), is(true));
205+
assertThat(ReservedRolesStore.isReserved(AsyncSearchUser.ROLE_NAME), is(true));
204206
assertThat(ReservedRolesStore.isReserved(LogstashSystemUser.ROLE_NAME), is(true));
205207
assertThat(ReservedRolesStore.isReserved(BeatsSystemUser.ROLE_NAME), is(true));
206208
assertThat(ReservedRolesStore.isReserved(APMSystemUser.ROLE_NAME), is(true));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
import org.elasticsearch.xpack.core.security.authc.Authentication;
1313
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
1414
import org.elasticsearch.xpack.core.security.support.Automatons;
15+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
1516
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
1617
import org.elasticsearch.xpack.core.security.user.XPackUser;
1718

1819
import java.util.function.Consumer;
1920
import java.util.function.Predicate;
2021

2122
import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN;
23+
import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN;
2224
import static org.elasticsearch.xpack.core.ClientHelper.ENRICH_ORIGIN;
2325
import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN;
2426
import static org.elasticsearch.xpack.core.ClientHelper.DEPRECATION_ORIGIN;
@@ -116,6 +118,9 @@ public static void switchUserBasedOnActionOriginAndExecute(ThreadContext threadC
116118
case TASKS_ORIGIN: // TODO use a more limited user for tasks
117119
securityContext.executeAsUser(XPackUser.INSTANCE, consumer, Version.CURRENT);
118120
break;
121+
case ASYNC_SEARCH_ORIGIN:
122+
securityContext.executeAsUser(AsyncSearchUser.INSTANCE, consumer, Version.CURRENT);
123+
break;
119124
default:
120125
assert false : "action.origin [" + actionOrigin + "] is unknown!";
121126
throw new IllegalStateException("action.origin [" + actionOrigin + "] should always be a known value");

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java

+5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper;
4545
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
4646
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
47+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
4748
import org.elasticsearch.xpack.core.security.user.SystemUser;
4849
import org.elasticsearch.xpack.core.security.user.User;
4950
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
@@ -212,6 +213,10 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
212213
roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE);
213214
return;
214215
}
216+
if (AsyncSearchUser.is(user)) {
217+
roleActionListener.onResponse(AsyncSearchUser.ROLE);
218+
return;
219+
}
215220

216221
final Authentication.AuthenticationType authType = authentication.getAuthenticationType();
217222
if (authType == Authentication.AuthenticationType.API_KEY) {

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
import org.elasticsearch.xpack.core.security.authc.Authentication;
1515
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
1616
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
17+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
1718
import org.elasticsearch.xpack.core.security.user.SystemUser;
1819
import org.elasticsearch.xpack.core.security.user.User;
1920
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
2021
import org.elasticsearch.xpack.core.security.user.XPackUser;
2122
import org.junit.Before;
2223

24+
import java.util.Arrays;
2325
import java.util.concurrent.CountDownLatch;
2426
import java.util.function.Consumer;
2527

@@ -95,9 +97,15 @@ public void testSwitchAndExecuteXpackSecurityUser() throws Exception {
9597
}
9698

9799
public void testSwitchAndExecuteXpackUser() throws Exception {
98-
String origin = randomFrom(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN,
99-
ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN);
100-
assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE);
100+
for (String origin : Arrays.asList(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN,
101+
ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN)) {
102+
assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE);
103+
}
104+
}
105+
106+
public void testSwitchAndExecuteAsyncSearchUser() throws Exception {
107+
String origin = ClientHelper.ASYNC_SEARCH_ORIGIN;
108+
assertSwitchBasedOnOriginAndExecute(origin, AsyncSearchUser.INSTANCE);
101109
}
102110

103111
public void testSwitchWithTaskOrigin() throws Exception {
@@ -124,10 +132,10 @@ private void assertSwitchBasedOnOriginAndExecute(String origin, User user) throw
124132
latch.countDown();
125133
listener.onResponse(null);
126134
};
135+
127136
threadContext.putHeader(headerName, headerValue);
128137
try (ThreadContext.StoredContext ignored = threadContext.stashWithOrigin(origin)) {
129138
AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer);
130-
131139
latch.await();
132140
}
133141
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
5757
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
5858
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
59+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
5960
import org.elasticsearch.xpack.core.security.user.SystemUser;
6061
import org.elasticsearch.xpack.core.security.user.User;
6162
import org.elasticsearch.xpack.core.security.user.XPackUser;
@@ -937,7 +938,7 @@ public void testAnonymousUserEnabledRoleAdded() {
937938
assertThat(Arrays.asList(roles.names()), hasItem("anonymous_user_role"));
938939
}
939940

940-
public void testDoesNotUseRolesStoreForXPackUser() {
941+
public void testDoesNotUseRolesStoreForXPacAndAsyncSearchUser() {
941942
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
942943
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
943944
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
@@ -959,13 +960,23 @@ public void testDoesNotUseRolesStoreForXPackUser() {
959960
rds -> effectiveRoleDescriptors.set(rds));
960961
verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
961962

963+
// test Xpack user short circuits to its own reserved role
962964
PlainActionFuture<Role> rolesFuture = new PlainActionFuture<>();
963965
Authentication auth = new Authentication(XPackUser.INSTANCE, new RealmRef("name", "type", "node"), null);
964966
compositeRolesStore.getRoles(XPackUser.INSTANCE, auth, rolesFuture);
965-
final Role roles = rolesFuture.actionGet();
967+
Role roles = rolesFuture.actionGet();
966968
assertThat(roles, equalTo(XPackUser.ROLE));
967969
assertThat(effectiveRoleDescriptors.get(), is(nullValue()));
968970
verifyNoMoreInteractions(fileRolesStore, nativeRolesStore, reservedRolesStore);
971+
972+
// test AyncSearch user short circuits to its own reserved role
973+
rolesFuture = new PlainActionFuture<>();
974+
auth = new Authentication(AsyncSearchUser.INSTANCE, new RealmRef("name", "type", "node"), null);
975+
compositeRolesStore.getRoles(AsyncSearchUser.INSTANCE, auth, rolesFuture);
976+
roles = rolesFuture.actionGet();
977+
assertThat(roles, equalTo(AsyncSearchUser.ROLE));
978+
assertThat(effectiveRoleDescriptors.get(), is(nullValue()));
979+
verifyNoMoreInteractions(fileRolesStore, nativeRolesStore, reservedRolesStore);
969980
}
970981

971982
public void testGetRolesForSystemUserThrowsException() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.security.user;
7+
8+
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsAction;
9+
import org.elasticsearch.action.admin.cluster.state.ClusterStateAction;
10+
import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction;
11+
import org.elasticsearch.action.delete.DeleteAction;
12+
import org.elasticsearch.action.get.GetAction;
13+
import org.elasticsearch.action.index.IndexAction;
14+
import org.elasticsearch.action.search.SearchAction;
15+
import org.elasticsearch.test.ESTestCase;
16+
import org.elasticsearch.transport.TransportRequest;
17+
import org.elasticsearch.xpack.core.security.authc.Authentication;
18+
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
19+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
20+
import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchAction;
21+
import org.hamcrest.Matchers;
22+
23+
import java.util.Arrays;
24+
import java.util.function.Predicate;
25+
26+
import static org.mockito.Mockito.mock;
27+
28+
public class AsyncSearchUserTests extends ESTestCase {
29+
30+
public void testAsyncSearchUserCannotAccessNonRestrictedIndices() {
31+
for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) {
32+
Predicate<String> predicate = AsyncSearchUser.ROLE.indices().allowedIndicesMatcher(action);
33+
String index = randomAlphaOfLengthBetween(3, 12);
34+
if (false == RestrictedIndicesNames.isRestricted(index)) {
35+
assertThat(predicate.test(index), Matchers.is(false));
36+
}
37+
index = "." + randomAlphaOfLengthBetween(3, 12);
38+
if (false == RestrictedIndicesNames.isRestricted(index)) {
39+
assertThat(predicate.test(index), Matchers.is(false));
40+
}
41+
}
42+
}
43+
44+
public void testAsyncSearchUserCanAccessOnlyAsyncSearchRestrictedIndices() {
45+
for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) {
46+
final Predicate<String> predicate = AsyncSearchUser.ROLE.indices().allowedIndicesMatcher(action);
47+
for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) {
48+
assertThat(predicate.test(index), Matchers.is(false));
49+
}
50+
assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 3)), Matchers.is(true));
51+
}
52+
}
53+
54+
public void testAsyncSearchUserHasNoClusterPrivileges() {
55+
for (String action : Arrays.asList(ClusterStateAction.NAME, GetWatchAction.NAME, ClusterStatsAction.NAME, NodesStatsAction.NAME)) {
56+
assertThat(AsyncSearchUser.ROLE.cluster().check(action, mock(TransportRequest.class), mock(Authentication.class)),
57+
Matchers.is(false));
58+
}
59+
}
60+
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/XPackUserTests.java

+19-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.security.user;
77

8+
import org.elasticsearch.action.delete.DeleteAction;
89
import org.elasticsearch.action.get.GetAction;
910
import org.elasticsearch.action.index.IndexAction;
1011
import org.elasticsearch.action.search.SearchAction;
@@ -17,24 +18,33 @@
1718
import org.hamcrest.Matchers;
1819
import org.joda.time.DateTime;
1920

21+
import java.util.Arrays;
2022
import java.util.function.Predicate;
2123

2224
public class XPackUserTests extends ESTestCase {
2325

2426
public void testXPackUserCanAccessNonSecurityIndices() {
25-
final String action = randomFrom(GetAction.NAME, SearchAction.NAME, IndexAction.NAME);
26-
final Predicate<String> predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action);
27-
final String index = randomBoolean() ? randomAlphaOfLengthBetween(3, 12) : "." + randomAlphaOfLength(8);
28-
assertThat(predicate.test(index), Matchers.is(true));
27+
for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) {
28+
Predicate<String> predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action);
29+
String index = randomAlphaOfLengthBetween(3, 12);
30+
if (false == RestrictedIndicesNames.isRestricted(index)) {
31+
assertThat(predicate.test(index), Matchers.is(true));
32+
}
33+
index = "." + randomAlphaOfLengthBetween(3, 12);
34+
if (false == RestrictedIndicesNames.isRestricted(index)) {
35+
assertThat(predicate.test(index), Matchers.is(true));
36+
}
37+
}
2938
}
3039

3140
public void testXPackUserCannotAccessRestrictedIndices() {
32-
final String action = randomFrom(GetAction.NAME, SearchAction.NAME, IndexAction.NAME);
33-
final Predicate<String> predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action);
34-
for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) {
35-
assertThat(predicate.test(index), Matchers.is(false));
41+
for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) {
42+
Predicate<String> predicate = XPackUser.ROLE.indices().allowedIndicesMatcher(action);
43+
for (String index : RestrictedIndicesNames.RESTRICTED_NAMES) {
44+
assertThat(predicate.test(index), Matchers.is(false));
45+
}
46+
assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)), Matchers.is(false));
3647
}
37-
assertThat(predicate.test(RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)), Matchers.is(false));
3848
}
3949

4050
public void testXPackUserCanReadAuditTrail() {

0 commit comments

Comments
 (0)