Skip to content

Commit a867749

Browse files
authored
[Backport] Add support for secondary authentication (#53530)
This change makes it possible to send secondary authentication credentials to select endpoints that need to perform a single action in the context of two users. Typically this need arises when a server process needs to call an endpoint that users should not (or might not) have direct access to, but some part of that action must be performed using the logged-in user's identity. Backport of: #52093
1 parent bac1740 commit a867749

File tree

13 files changed

+953
-44
lines changed

13 files changed

+953
-44
lines changed

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

+27
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import org.elasticsearch.xpack.core.security.authc.Authentication;
1616
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
1717
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
18+
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
1819
import org.elasticsearch.xpack.core.security.user.User;
1920

2021
import java.io.IOException;
2122
import java.io.UncheckedIOException;
2223
import java.util.Collections;
2324
import java.util.Objects;
2425
import java.util.function.Consumer;
26+
import java.util.function.Function;
2527

2628
/**
2729
* A lightweight utility that can find the current user and authentication information for the local thread.
@@ -55,6 +57,19 @@ public Authentication getAuthentication() {
5557
}
5658
}
5759

60+
/**
61+
* Returns the "secondary authentication" (see {@link SecondaryAuthentication}) information,
62+
* or {@code null} if the current request does not have a secondary authentication context
63+
*/
64+
public SecondaryAuthentication getSecondaryAuthentication() {
65+
try {
66+
return SecondaryAuthentication.readFromContext(this);
67+
} catch (IOException e) {
68+
logger.error("failed to read secondary authentication", e);
69+
throw new UncheckedIOException(e);
70+
}
71+
}
72+
5873
public ThreadContext getThreadContext() {
5974
return threadContext;
6075
}
@@ -97,6 +112,18 @@ public void executeAsUser(User user, Consumer<StoredContext> consumer, Version v
97112
}
98113
}
99114

115+
/**
116+
* Runs the consumer in a new context as the provided user. The original context is provided to the consumer. When this method
117+
* returns, the original context is restored.
118+
*/
119+
public <T> T executeWithAuthentication(Authentication authentication, Function<StoredContext, T> consumer) {
120+
final StoredContext original = threadContext.newStoredContext(true);
121+
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
122+
setAuthentication(authentication);
123+
return consumer.apply(original);
124+
}
125+
}
126+
100127
/**
101128
* Runs the consumer in a new context after setting a new version of the authentication that is compatible with the version provided.
102129
* The original context is provided to the consumer. When this method returns, the original context is restored.

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ public int hashCode() {
150150
@Override
151151
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
152152
builder.startObject();
153+
toXContentFragment(builder);
154+
return builder.endObject();
155+
}
156+
157+
/**
158+
* Generates XContent without the start/end object.
159+
*/
160+
public void toXContentFragment(XContentBuilder builder) throws IOException {
153161
builder.field(User.Fields.USERNAME.getPreferredName(), user.principal());
154162
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
155163
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
@@ -169,7 +177,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
169177
builder.field(User.Fields.REALM_TYPE.getPreferredName(), getAuthenticatedBy().getType());
170178
}
171179
builder.endObject();
172-
return builder.endObject();
173180
}
174181

175182
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/AuthenticationContextSerializer.java

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public Authentication getAuthentication(ThreadContext context) {
7575
public void writeToContext(Authentication authentication, ThreadContext ctx) throws IOException {
7676
ensureContextDoesNotContainAuthentication(ctx);
7777
String header = authentication.encode();
78+
assert header != null : "Authentication object encoded to null"; // this usually happens with mock objects in tests
7879
ctx.putTransient(contextKey, authentication);
7980
ctx.putHeader(contextKey, header);
8081
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
7+
package org.elasticsearch.xpack.core.security.authc.support;
8+
9+
import org.elasticsearch.common.Nullable;
10+
import org.elasticsearch.common.util.concurrent.ThreadContext;
11+
import org.elasticsearch.xpack.core.security.SecurityContext;
12+
import org.elasticsearch.xpack.core.security.authc.Authentication;
13+
14+
import java.io.IOException;
15+
import java.util.Objects;
16+
import java.util.function.Function;
17+
18+
/**
19+
* Some Elasticsearch APIs need to be provided with 2 sets of credentials.
20+
* Typically this happens when a system user needs to perform an action while accessing data on behalf of, or user information regarding
21+
* a logged in user.
22+
* This class is a representation of that secondary user that can be activated in the security context while processing specific blocks
23+
* of code or within a listener.
24+
*/
25+
public class SecondaryAuthentication {
26+
27+
private static final String THREAD_CTX_KEY = "_xpack_security_secondary_authc";
28+
29+
private final SecurityContext securityContext;
30+
private final Authentication authentication;
31+
32+
public SecondaryAuthentication(SecurityContext securityContext, Authentication authentication) {
33+
this.securityContext = Objects.requireNonNull(securityContext);
34+
this.authentication = Objects.requireNonNull(authentication);
35+
}
36+
37+
@Nullable
38+
public static SecondaryAuthentication readFromContext(SecurityContext securityContext) throws IOException {
39+
final Authentication authentication = serializer().readFromContext(securityContext.getThreadContext());
40+
if (authentication == null) {
41+
return null;
42+
}
43+
return new SecondaryAuthentication(securityContext, authentication);
44+
}
45+
46+
public void writeToContext(ThreadContext threadContext) throws IOException {
47+
serializer().writeToContext(this.authentication, threadContext);
48+
}
49+
50+
private static AuthenticationContextSerializer serializer() {
51+
return new AuthenticationContextSerializer(THREAD_CTX_KEY);
52+
}
53+
54+
public Authentication getAuthentication() {
55+
return authentication;
56+
}
57+
58+
public <T> T execute(Function<ThreadContext.StoredContext, T> body) {
59+
return this.securityContext.executeWithAuthentication(this.authentication, body);
60+
}
61+
62+
public Runnable wrap(Runnable runnable) {
63+
return () -> execute(ignore -> {
64+
runnable.run();
65+
return null;
66+
});
67+
}
68+
69+
@Override
70+
public String toString() {
71+
return getClass().getSimpleName() + "{" + authentication + "}";
72+
}
73+
74+
@Override
75+
public boolean equals(Object o) {
76+
if (this == o) return true;
77+
if (o == null || getClass() != o.getClass()) return false;
78+
final SecondaryAuthentication that = (SecondaryAuthentication) o;
79+
return authentication.equals(that.authentication);
80+
}
81+
82+
@Override
83+
public int hashCode() {
84+
return Objects.hash(authentication);
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
7+
package org.elasticsearch.xpack.core.security.authc.support;
8+
9+
import org.elasticsearch.Version;
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.action.support.ContextPreservingActionListener;
12+
import org.elasticsearch.common.settings.Settings;
13+
import org.elasticsearch.common.util.concurrent.ThreadContext;
14+
import org.elasticsearch.test.ESTestCase;
15+
import org.elasticsearch.threadpool.TestThreadPool;
16+
import org.elasticsearch.threadpool.ThreadPool;
17+
import org.elasticsearch.xpack.core.security.SecurityContext;
18+
import org.elasticsearch.xpack.core.security.authc.Authentication;
19+
import org.elasticsearch.xpack.core.security.user.User;
20+
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
21+
import org.junit.After;
22+
import org.junit.Before;
23+
24+
import java.util.concurrent.Future;
25+
import java.util.concurrent.Semaphore;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
29+
import static org.hamcrest.Matchers.equalTo;
30+
import static org.hamcrest.Matchers.notNullValue;
31+
import static org.hamcrest.Matchers.sameInstance;
32+
33+
public class SecondaryAuthenticationTests extends ESTestCase {
34+
35+
private SecurityContext securityContext;
36+
private ThreadPool threadPool;
37+
38+
@Before
39+
public void setupObjects() {
40+
this.threadPool = new TestThreadPool(getTestName());
41+
this.securityContext = new SecurityContext(Settings.EMPTY, threadPool.getThreadContext());
42+
}
43+
44+
@After
45+
public void cleanup() {
46+
this.threadPool.shutdownNow();
47+
}
48+
49+
public void testCannotCreateObjectWithNullAuthentication() {
50+
expectThrows(NullPointerException.class, () -> new SecondaryAuthentication(securityContext, null));
51+
}
52+
53+
public void testSynchronousExecuteInSecondaryContext() {
54+
final User user1 = new User("u1", "role1");
55+
securityContext.setUser(user1, Version.CURRENT);
56+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
57+
58+
final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm());
59+
final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2);
60+
61+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
62+
String result = secondaryAuth.execute(original -> {
63+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
64+
assertThat(securityContext.getAuthentication(), sameInstance(authentication2));
65+
return "xyzzy";
66+
});
67+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
68+
assertThat(result, equalTo("xyzzy"));
69+
}
70+
71+
public void testSecondaryContextCanBeRestored() {
72+
final User user1 = new User("u1", "role1");
73+
securityContext.setUser(user1, Version.CURRENT);
74+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
75+
76+
final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm());
77+
final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2);
78+
79+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
80+
final AtomicReference<ThreadContext.StoredContext> secondaryContext = new AtomicReference<>();
81+
secondaryAuth.execute(storedContext -> {
82+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
83+
assertThat(securityContext.getAuthentication(), sameInstance(authentication2));
84+
secondaryContext.set(threadPool.getThreadContext().newStoredContext(false));
85+
storedContext.restore();
86+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
87+
return null;
88+
});
89+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
90+
secondaryContext.get().restore();
91+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
92+
}
93+
94+
public void testWrapRunnable() throws Exception {
95+
final User user1 = new User("u1", "role1");
96+
securityContext.setUser(user1, Version.CURRENT);
97+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
98+
99+
final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm());
100+
final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2);
101+
102+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
103+
final Semaphore semaphore = new Semaphore(0);
104+
final Future<?> future = threadPool.generic().submit(secondaryAuth.wrap(() -> {
105+
try {
106+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
107+
semaphore.acquire();
108+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
109+
semaphore.acquire();
110+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
111+
} catch (InterruptedException e) {
112+
Thread.currentThread().interrupt();
113+
throw new RuntimeException(e);
114+
}
115+
}));
116+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
117+
semaphore.release();
118+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
119+
semaphore.release();
120+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
121+
122+
// ensure that the runnable didn't throw any exceptions / assertions
123+
future.get(1, TimeUnit.SECONDS);
124+
}
125+
126+
public void testPreserveSecondaryContextAcrossThreads() throws Exception {
127+
final User user1 = new User("u1", "role1");
128+
securityContext.setUser(user1, Version.CURRENT);
129+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
130+
131+
final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm());
132+
final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2);
133+
134+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
135+
136+
final AtomicReference<User> threadUser = new AtomicReference<>();
137+
final AtomicReference<User> listenerUser = new AtomicReference<>();
138+
139+
final ThreadContext threadContext = threadPool.getThreadContext();
140+
secondaryAuth.execute(originalContext -> {
141+
assertThat(securityContext.getUser().principal(), equalTo("u2"));
142+
ActionListener<Void> listener = new ContextPreservingActionListener<>(threadContext.newRestorableContext(false),
143+
ActionListener.wrap(() -> listenerUser.set(securityContext.getUser())));
144+
originalContext.restore();
145+
threadPool.generic().execute(() -> {
146+
threadUser.set(securityContext.getUser());
147+
listener.onResponse(null);
148+
});
149+
return null;
150+
});
151+
assertThat(securityContext.getUser().principal(), equalTo("u1"));
152+
assertBusy(() -> assertThat(listenerUser.get(), notNullValue()), 1, TimeUnit.SECONDS);
153+
assertThat(threadUser.get(), notNullValue());
154+
assertThat(threadUser.get().principal(), equalTo("u1"));
155+
assertThat(listenerUser.get().principal(), equalTo("u2"));
156+
}
157+
158+
private Authentication.RealmRef realm() {
159+
return new Authentication.RealmRef(randomAlphaOfLengthBetween(4, 8), randomAlphaOfLengthBetween(2, 4), randomAlphaOfLength(12));
160+
}
161+
162+
}

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
import org.elasticsearch.xpack.security.authc.TokenService;
185185
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
186186
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
187+
import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator;
187188
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
188189
import org.elasticsearch.xpack.security.authz.AuthorizationService;
189190
import org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener;
@@ -288,6 +289,7 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin,
288289
private final SetOnce<TransportInterceptor> securityInterceptor = new SetOnce<>();
289290
private final SetOnce<IPFilter> ipFilter = new SetOnce<>();
290291
private final SetOnce<AuthenticationService> authcService = new SetOnce<>();
292+
private final SetOnce<SecondaryAuthenticator> secondayAuthc = new SetOnce<>();
291293
private final SetOnce<AuditTrailService> auditTrailService = new SetOnce<>();
292294
private final SetOnce<SecurityContext> securityContext = new SetOnce<>();
293295
private final SetOnce<ThreadContext> threadContext = new SetOnce<>();
@@ -515,6 +517,10 @@ auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEn
515517
components.add(allRolesStore); // for SecurityFeatureSet and clear roles cache
516518
components.add(authzService);
517519

520+
final SecondaryAuthenticator secondaryAuthenticator = new SecondaryAuthenticator(securityContext.get(), authcService.get());
521+
this.secondayAuthc.set(secondaryAuthenticator);
522+
components.add(secondaryAuthenticator);
523+
518524
ipFilter.set(new IPFilter(settings, auditTrailService, clusterService.getClusterSettings(), getLicenseState()));
519525
components.add(ipFilter.get());
520526
DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings());
@@ -688,6 +694,7 @@ public Collection<RestHeaderDefinition> getRestHeaders() {
688694
}
689695
Set<RestHeaderDefinition> headers = new HashSet<>();
690696
headers.add(new RestHeaderDefinition(UsernamePasswordToken.BASIC_AUTH_HEADER, false));
697+
headers.add(new RestHeaderDefinition(SecondaryAuthenticator.SECONDARY_AUTH_HEADER_NAME, false));
691698
if (XPackSettings.AUDIT_ENABLED.get(settings)) {
692699
headers.add(new RestHeaderDefinition(AuditTrail.X_FORWARDED_FOR_HEADER, true));
693700
}
@@ -996,7 +1003,8 @@ public UnaryOperator<RestHandler> getRestHandlerWrapper(ThreadContext threadCont
9961003
final boolean ssl = HTTP_SSL_ENABLED.get(settings);
9971004
final SSLConfiguration httpSSLConfig = getSslService().getHttpTransportSSLConfiguration();
9981005
boolean extractClientCertificate = ssl && getSslService().isSSLClientAuthEnabled(httpSSLConfig);
999-
return handler -> new SecurityRestFilter(getLicenseState(), threadContext, authcService.get(), handler, extractClientCertificate);
1006+
return handler -> new SecurityRestFilter(getLicenseState(), threadContext, authcService.get(), secondayAuthc.get(),
1007+
handler, extractClientCertificate);
10001008
}
10011009

10021010
@Override

0 commit comments

Comments
 (0)