Skip to content

Commit e7d1406

Browse files
authored
Ensure authentication is wire compatible when setting user (#86741) (#86828)
* Ensure authentication is wire compatible when setting user (#86741) The SecurityServerTransportInterceptor class is responsible for writing authentication header in a wire compatible format before the request leaving the local node. However, a bug made it ignore the wire version when setting user based on the action origin. This PR fixes it and adds relevant tests. It is an old bug but never manifested itself previously because (1) the code path is rare enuough and (2) authentication didn't have any version difference till 8.2. Resolves: #86716 * fix compilation
1 parent 5a45a88 commit e7d1406

File tree

6 files changed

+97
-11
lines changed

6 files changed

+97
-11
lines changed

docs/changelog/86741.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 86741
2+
summary: Ensure authentication is wire compatible when setting user
3+
area: Authentication
4+
type: bug
5+
issues:
6+
- 86716

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.apache.logging.log4j.LogManager;
1010
import org.apache.logging.log4j.Logger;
11+
import org.elasticsearch.Version;
1112
import org.elasticsearch.action.ActionListener;
1213
import org.elasticsearch.action.ActionRequest;
1314
import org.elasticsearch.action.ActionResponse;
@@ -105,6 +106,7 @@ operations are blocked on license expiration. All data operations (read and writ
105106
AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(
106107
threadContext,
107108
securityContext,
109+
Version.CURRENT, // current version since this is on the same node
108110
(original) -> { applyInternal(task, chain, action, request, contextPreservingListener); }
109111
);
110112
} else {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public static boolean shouldSetUserBasedOnActionOrigin(ThreadContext context) {
105105
public static void switchUserBasedOnActionOriginAndExecute(
106106
ThreadContext threadContext,
107107
SecurityContext securityContext,
108+
Version version,
108109
Consumer<ThreadContext.StoredContext> consumer
109110
) {
110111
final String actionOrigin = threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME);
@@ -115,7 +116,7 @@ public static void switchUserBasedOnActionOriginAndExecute(
115116

116117
switch (actionOrigin) {
117118
case SECURITY_ORIGIN:
118-
securityContext.executeAsInternalUser(XPackSecurityUser.INSTANCE, Version.CURRENT, consumer);
119+
securityContext.executeAsInternalUser(XPackSecurityUser.INSTANCE, version, consumer);
119120
break;
120121
case WATCHER_ORIGIN:
121122
case ML_ORIGIN:
@@ -133,10 +134,10 @@ public static void switchUserBasedOnActionOriginAndExecute(
133134
case LOGSTASH_MANAGEMENT_ORIGIN:
134135
case FLEET_ORIGIN:
135136
case TASKS_ORIGIN: // TODO use a more limited user for tasks
136-
securityContext.executeAsInternalUser(XPackUser.INSTANCE, Version.CURRENT, consumer);
137+
securityContext.executeAsInternalUser(XPackUser.INSTANCE, version, consumer);
137138
break;
138139
case ASYNC_SEARCH_ORIGIN:
139-
securityContext.executeAsInternalUser(AsyncSearchUser.INSTANCE, Version.CURRENT, consumer);
140+
securityContext.executeAsInternalUser(AsyncSearchUser.INSTANCE, version, consumer);
140141
break;
141142
default:
142143
assert false : "action.origin [" + actionOrigin + "] is unknown!";

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ public <T extends TransportResponse> void sendRequest(
106106
AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(
107107
threadPool.getThreadContext(),
108108
securityContext,
109+
minVersion,
109110
(original) -> sendWithUser(
110111
connection,
111112
action,

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
*/
77
package org.elasticsearch.xpack.security.authz;
88

9+
import org.elasticsearch.Version;
910
import org.elasticsearch.action.ActionListener;
1011
import org.elasticsearch.common.settings.Settings;
1112
import org.elasticsearch.common.util.concurrent.ThreadContext;
1213
import org.elasticsearch.persistent.PersistentTasksService;
1314
import org.elasticsearch.test.ESTestCase;
15+
import org.elasticsearch.test.VersionUtils;
1416
import org.elasticsearch.xpack.core.ClientHelper;
1517
import org.elasticsearch.xpack.core.security.SecurityContext;
1618
import org.elasticsearch.xpack.core.security.authc.Authentication;
@@ -98,7 +100,7 @@ public void testShouldSetUser() {
98100
}
99101

100102
public void testSwitchAndExecuteXpackSecurityUser() throws Exception {
101-
assertSwitchBasedOnOriginAndExecute(ClientHelper.SECURITY_ORIGIN, XPackSecurityUser.INSTANCE);
103+
assertSwitchBasedOnOriginAndExecute(ClientHelper.SECURITY_ORIGIN, XPackSecurityUser.INSTANCE, randomVersion());
102104
}
103105

104106
public void testSwitchAndExecuteXpackUser() throws Exception {
@@ -110,20 +112,20 @@ public void testSwitchAndExecuteXpackUser() throws Exception {
110112
PersistentTasksService.PERSISTENT_TASK_ORIGIN,
111113
ClientHelper.INDEX_LIFECYCLE_ORIGIN
112114
)) {
113-
assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE);
115+
assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE, randomVersion());
114116
}
115117
}
116118

117119
public void testSwitchAndExecuteAsyncSearchUser() throws Exception {
118120
String origin = ClientHelper.ASYNC_SEARCH_ORIGIN;
119-
assertSwitchBasedOnOriginAndExecute(origin, AsyncSearchUser.INSTANCE);
121+
assertSwitchBasedOnOriginAndExecute(origin, AsyncSearchUser.INSTANCE, randomVersion());
120122
}
121123

122124
public void testSwitchWithTaskOrigin() throws Exception {
123-
assertSwitchBasedOnOriginAndExecute(TASKS_ORIGIN, XPackUser.INSTANCE);
125+
assertSwitchBasedOnOriginAndExecute(TASKS_ORIGIN, XPackUser.INSTANCE, randomVersion());
124126
}
125127

126-
private void assertSwitchBasedOnOriginAndExecute(String origin, User user) throws Exception {
128+
private void assertSwitchBasedOnOriginAndExecute(String origin, User user, Version version) throws Exception {
127129
SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext);
128130
final String headerName = randomAlphaOfLengthBetween(4, 16);
129131
final String headerValue = randomAlphaOfLengthBetween(4, 16);
@@ -132,23 +134,30 @@ private void assertSwitchBasedOnOriginAndExecute(String origin, User user) throw
132134
final ActionListener<Void> listener = ActionListener.wrap(v -> {
133135
assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME));
134136
assertNull(threadContext.getHeader(headerName));
135-
assertEquals(user, securityContext.getAuthentication().getUser());
137+
final Authentication authentication = securityContext.getAuthentication();
138+
assertEquals(user, authentication.getUser());
139+
assertEquals(version, authentication.getVersion());
136140
latch.countDown();
137141
}, e -> fail(e.getMessage()));
138142

139143
final Consumer<ThreadContext.StoredContext> consumer = original -> {
140144
assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME));
141145
assertNull(threadContext.getHeader(headerName));
142-
assertEquals(user, securityContext.getAuthentication().getUser());
146+
final Authentication authentication = securityContext.getAuthentication();
147+
assertEquals(user, authentication.getUser());
148+
assertEquals(version, authentication.getVersion());
143149
latch.countDown();
144150
listener.onResponse(null);
145151
};
146152

147153
threadContext.putHeader(headerName, headerValue);
148154
try (ThreadContext.StoredContext ignored = threadContext.stashWithOrigin(origin)) {
149-
AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer);
155+
AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, version, consumer);
150156
latch.await();
151157
}
152158
}
153159

160+
private Version randomVersion() {
161+
return VersionUtils.randomCompatibleVersion(random(), Version.CURRENT);
162+
}
154163
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.tasks.Task;
1818
import org.elasticsearch.test.ClusterServiceUtils;
1919
import org.elasticsearch.test.ESTestCase;
20+
import org.elasticsearch.test.VersionUtils;
2021
import org.elasticsearch.threadpool.TestThreadPool;
2122
import org.elasticsearch.threadpool.ThreadPool;
2223
import org.elasticsearch.transport.Transport;
@@ -34,8 +35,11 @@
3435
import org.elasticsearch.xpack.core.security.authc.Authentication;
3536
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
3637
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
38+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
3739
import org.elasticsearch.xpack.core.security.user.SystemUser;
3840
import org.elasticsearch.xpack.core.security.user.User;
41+
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
42+
import org.elasticsearch.xpack.core.security.user.XPackUser;
3943
import org.elasticsearch.xpack.core.ssl.SSLService;
4044
import org.elasticsearch.xpack.security.authc.AuthenticationService;
4145
import org.elasticsearch.xpack.security.authz.AuthorizationService;
@@ -48,6 +52,12 @@
4852
import java.util.concurrent.atomic.AtomicReference;
4953
import java.util.function.Consumer;
5054

55+
import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN;
56+
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
57+
import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN;
58+
import static org.hamcrest.Matchers.equalTo;
59+
import static org.hamcrest.Matchers.is;
60+
import static org.hamcrest.Matchers.notNullValue;
5161
import static org.mockito.ArgumentMatchers.any;
5262
import static org.mockito.ArgumentMatchers.eq;
5363
import static org.mockito.Mockito.doAnswer;
@@ -324,6 +334,63 @@ public <T extends TransportResponse> void sendRequest(
324334
assertEquals(Version.CURRENT, authentication.getVersion());
325335
}
326336

337+
public void testSetUserBasedOnActionOrigin() {
338+
final Map<String, User> originToUserMap = Map.of(
339+
SECURITY_ORIGIN,
340+
XPackSecurityUser.INSTANCE,
341+
TRANSFORM_ORIGIN,
342+
XPackUser.INSTANCE,
343+
ASYNC_SEARCH_ORIGIN,
344+
AsyncSearchUser.INSTANCE
345+
);
346+
347+
final String origin = randomFrom(originToUserMap.keySet());
348+
349+
threadContext.putTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME, origin);
350+
SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor(
351+
settings,
352+
threadPool,
353+
mock(AuthenticationService.class),
354+
mock(AuthorizationService.class),
355+
mock(SSLService.class),
356+
securityContext,
357+
new DestructiveOperations(
358+
Settings.EMPTY,
359+
new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING))
360+
)
361+
);
362+
363+
final AtomicBoolean calledWrappedSender = new AtomicBoolean(false);
364+
final AtomicReference<Authentication> authenticationRef = new AtomicReference<>();
365+
final AsyncSender intercepted = new AsyncSender() {
366+
@Override
367+
public <T extends TransportResponse> void sendRequest(
368+
Transport.Connection connection,
369+
String action,
370+
TransportRequest request,
371+
TransportRequestOptions options,
372+
TransportResponseHandler<T> handler
373+
) {
374+
if (calledWrappedSender.compareAndSet(false, true) == false) {
375+
fail("sender called more than once!");
376+
}
377+
authenticationRef.set(securityContext.getAuthentication());
378+
}
379+
};
380+
final AsyncSender sender = interceptor.interceptSender(intercepted);
381+
382+
Transport.Connection connection = mock(Transport.Connection.class);
383+
final Version connectionVersion = VersionUtils.randomCompatibleVersion(random(), Version.CURRENT);
384+
when(connection.getVersion()).thenReturn(connectionVersion);
385+
386+
sender.sendRequest(connection, "indices:foo[s]", null, null, null);
387+
assertThat(calledWrappedSender.get(), is(true));
388+
final Authentication authentication = authenticationRef.get();
389+
assertThat(authentication, notNullValue());
390+
assertThat(authentication.getUser(), equalTo(originToUserMap.get(origin)));
391+
assertThat(authentication.getVersion(), equalTo(connectionVersion));
392+
}
393+
327394
public void testContextRestoreResponseHandler() throws Exception {
328395
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
329396

0 commit comments

Comments
 (0)