Skip to content

Commit 9fb5c81

Browse files
authored
Extract class to store Authentication in context (#52032)
This change extracts the code that previously existed in the "Authentication" class that was responsible for reading and writing authentication objects to/from the ThreadContext. This is needed to support multiple authentication objects under separate keys. This refactoring highlighted that there were a large number of places where we extracted the Authentication/User objects from the thread context, in a variety of ways. These have been consolidated to rely on the SecurityContext object.
1 parent 7cebcdf commit 9fb5c81

32 files changed

+359
-254
lines changed

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

+9-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.node.Node;
1515
import org.elasticsearch.xpack.core.security.authc.Authentication;
1616
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
17+
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
1718
import org.elasticsearch.xpack.core.security.user.User;
1819

1920
import java.io.IOException;
@@ -29,17 +30,12 @@ public class SecurityContext {
2930
private final Logger logger = LogManager.getLogger(SecurityContext.class);
3031

3132
private final ThreadContext threadContext;
32-
private final UserSettings userSettings;
33+
private final AuthenticationContextSerializer authenticationSerializer;
3334
private final String nodeName;
3435

35-
/**
36-
* Creates a new security context.
37-
* If cryptoService is null, security is disabled and {@link UserSettings#getUser()}
38-
* and {@link UserSettings#getAuthentication()} will always return null.
39-
*/
4036
public SecurityContext(Settings settings, ThreadContext threadContext) {
4137
this.threadContext = threadContext;
42-
this.userSettings = new UserSettings(threadContext);
38+
this.authenticationSerializer = new AuthenticationContextSerializer();
4339
this.nodeName = Node.NODE_NAME_SETTING.get(settings);
4440
}
4541

@@ -52,13 +48,17 @@ public User getUser() {
5248
/** Returns the authentication information, or null if the current request has no authentication info. */
5349
public Authentication getAuthentication() {
5450
try {
55-
return Authentication.readFromContext(threadContext);
51+
return authenticationSerializer.readFromContext(threadContext);
5652
} catch (IOException e) {
5753
logger.error("failed to read authentication", e);
5854
throw new UncheckedIOException(e);
5955
}
6056
}
6157

58+
public ThreadContext getThreadContext() {
59+
return threadContext;
60+
}
61+
6262
/**
6363
* Sets the user forcefully to the provided user. There must not be an existing user in the ThreadContext otherwise an exception
6464
* will be thrown. This method is package private for testing.
@@ -103,7 +103,7 @@ public void executeAsUser(User user, Consumer<StoredContext> consumer, Version v
103103
*/
104104
public void executeAfterRewritingAuthentication(Consumer<StoredContext> consumer, Version version) {
105105
final StoredContext original = threadContext.newStoredContext(true);
106-
final Authentication authentication = Objects.requireNonNull(userSettings.getAuthentication());
106+
final Authentication authentication = getAuthentication();
107107
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
108108
setAuthentication(new Authentication(authentication.getUser(), authentication.getAuthenticatedBy(),
109109
authentication.getLookedUpBy(), version, authentication.getAuthenticationType(), authentication.getMetadata()));

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

-46
This file was deleted.

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

+2-48
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.common.util.concurrent.ThreadContext;
1414
import org.elasticsearch.common.xcontent.ToXContentObject;
1515
import org.elasticsearch.common.xcontent.XContentBuilder;
16+
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
1617
import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper;
1718
import org.elasticsearch.xpack.core.security.user.User;
1819

@@ -92,59 +93,12 @@ public Map<String, Object> getMetadata() {
9293
return metadata;
9394
}
9495

95-
public static Authentication readFromContext(ThreadContext ctx) throws IOException, IllegalArgumentException {
96-
Authentication authentication = ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY);
97-
if (authentication != null) {
98-
assert ctx.getHeader(AuthenticationField.AUTHENTICATION_KEY) != null;
99-
return authentication;
100-
}
101-
102-
String authenticationHeader = ctx.getHeader(AuthenticationField.AUTHENTICATION_KEY);
103-
if (authenticationHeader == null) {
104-
return null;
105-
}
106-
return deserializeHeaderAndPutInContext(authenticationHeader, ctx);
107-
}
108-
109-
public static Authentication getAuthentication(ThreadContext context) {
110-
return context.getTransient(AuthenticationField.AUTHENTICATION_KEY);
111-
}
112-
113-
static Authentication deserializeHeaderAndPutInContext(String header, ThreadContext ctx)
114-
throws IOException, IllegalArgumentException {
115-
assert ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY) == null;
116-
117-
Authentication authentication = decode(header);
118-
ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
119-
return authentication;
120-
}
121-
122-
public static Authentication decode(String header) throws IOException {
123-
byte[] bytes = Base64.getDecoder().decode(header);
124-
StreamInput input = StreamInput.wrap(bytes);
125-
Version version = Version.readVersion(input);
126-
input.setVersion(version);
127-
return new Authentication(input);
128-
}
129-
13096
/**
13197
* Writes the authentication to the context. There must not be an existing authentication in the context and if there is an
13298
* {@link IllegalStateException} will be thrown
13399
*/
134100
public void writeToContext(ThreadContext ctx) throws IOException, IllegalArgumentException {
135-
ensureContextDoesNotContainAuthentication(ctx);
136-
String header = encode();
137-
ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, this);
138-
ctx.putHeader(AuthenticationField.AUTHENTICATION_KEY, header);
139-
}
140-
141-
void ensureContextDoesNotContainAuthentication(ThreadContext ctx) {
142-
if (ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY) != null) {
143-
if (ctx.getHeader(AuthenticationField.AUTHENTICATION_KEY) == null) {
144-
throw new IllegalStateException("authentication present as a transient but not a header");
145-
}
146-
throw new IllegalStateException("authentication is already present in the context");
147-
}
101+
new AuthenticationContextSerializer().writeToContext(this, ctx);
148102
}
149103

150104
public String encode() throws IOException {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.common.Nullable;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.util.concurrent.ThreadContext;
13+
import org.elasticsearch.xpack.core.security.authc.Authentication;
14+
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
15+
16+
import java.io.IOException;
17+
import java.util.Base64;
18+
19+
/**
20+
* A class from reading/writing {@link org.elasticsearch.xpack.core.security.authc.Authentication} objects to/from a
21+
* {@link org.elasticsearch.common.util.concurrent.ThreadContext} under a specified key
22+
*/
23+
public class AuthenticationContextSerializer {
24+
25+
private final String contextKey;
26+
27+
public AuthenticationContextSerializer() {
28+
this(AuthenticationField.AUTHENTICATION_KEY);
29+
}
30+
31+
public AuthenticationContextSerializer(String contextKey) {
32+
this.contextKey = contextKey;
33+
}
34+
35+
@Nullable
36+
public Authentication readFromContext(ThreadContext ctx) throws IOException {
37+
Authentication authentication = ctx.getTransient(contextKey);
38+
if (authentication != null) {
39+
assert ctx.getHeader(contextKey) != null;
40+
return authentication;
41+
}
42+
43+
String authenticationHeader = ctx.getHeader(contextKey);
44+
if (authenticationHeader == null) {
45+
return null;
46+
}
47+
return deserializeHeaderAndPutInContext(authenticationHeader, ctx);
48+
}
49+
50+
Authentication deserializeHeaderAndPutInContext(String headerValue, ThreadContext ctx)
51+
throws IOException, IllegalArgumentException {
52+
assert ctx.getTransient(contextKey) == null;
53+
54+
Authentication authentication = decode(headerValue);
55+
ctx.putTransient(contextKey, authentication);
56+
return authentication;
57+
}
58+
59+
public static Authentication decode(String header) throws IOException {
60+
byte[] bytes = Base64.getDecoder().decode(header);
61+
StreamInput input = StreamInput.wrap(bytes);
62+
Version version = Version.readVersion(input);
63+
input.setVersion(version);
64+
return new Authentication(input);
65+
}
66+
67+
public Authentication getAuthentication(ThreadContext context) {
68+
return context.getTransient(contextKey);
69+
}
70+
71+
/**
72+
* Writes the authentication to the context. There must not be an existing authentication in the context and if there is an
73+
* {@link IllegalStateException} will be thrown
74+
*/
75+
public void writeToContext(Authentication authentication, ThreadContext ctx) throws IOException {
76+
ensureContextDoesNotContainAuthentication(ctx);
77+
String header = authentication.encode();
78+
ctx.putTransient(contextKey, authentication);
79+
ctx.putHeader(contextKey, header);
80+
}
81+
82+
void ensureContextDoesNotContainAuthentication(ThreadContext ctx) {
83+
if (ctx.getTransient(contextKey) != null) {
84+
if (ctx.getHeader(contextKey) == null) {
85+
throw new IllegalStateException("authentication present as a transient ([" + contextKey + "]) but not a header");
86+
}
87+
throw new IllegalStateException("authentication ([" + contextKey + "]) is already present in the context");
88+
}
89+
}
90+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java

+9-8
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
import org.elasticsearch.index.shard.ShardUtils;
2020
import org.elasticsearch.license.XPackLicenseState;
2121
import org.elasticsearch.script.ScriptService;
22-
import org.elasticsearch.xpack.core.security.authc.Authentication;
22+
import org.elasticsearch.xpack.core.security.SecurityContext;
2323
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
2424
import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions;
2525
import org.elasticsearch.xpack.core.security.support.Exceptions;
2626
import org.elasticsearch.xpack.core.security.user.User;
2727

2828
import java.io.IOException;
29+
import java.util.Objects;
2930
import java.util.function.Function;
3031

3132
/**
@@ -45,16 +46,16 @@ public class SecurityIndexReaderWrapper implements CheckedFunction<DirectoryRead
4546
private final Function<ShardId, QueryShardContext> queryShardContextProvider;
4647
private final DocumentSubsetBitsetCache bitsetCache;
4748
private final XPackLicenseState licenseState;
48-
private final ThreadContext threadContext;
49+
private final SecurityContext securityContext;
4950
private final ScriptService scriptService;
5051

5152
public SecurityIndexReaderWrapper(Function<ShardId, QueryShardContext> queryShardContextProvider,
52-
DocumentSubsetBitsetCache bitsetCache, ThreadContext threadContext, XPackLicenseState licenseState,
53-
ScriptService scriptService) {
53+
DocumentSubsetBitsetCache bitsetCache, SecurityContext securityContext,
54+
XPackLicenseState licenseState, ScriptService scriptService) {
5455
this.scriptService = scriptService;
5556
this.queryShardContextProvider = queryShardContextProvider;
5657
this.bitsetCache = bitsetCache;
57-
this.threadContext = threadContext;
58+
this.securityContext = securityContext;
5859
this.licenseState = licenseState;
5960
}
6061

@@ -95,16 +96,16 @@ public DirectoryReader apply(final DirectoryReader reader) {
9596
}
9697

9798
protected IndicesAccessControl getIndicesAccessControl() {
99+
final ThreadContext threadContext = securityContext.getThreadContext();
98100
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
99101
if (indicesAccessControl == null) {
100102
throw Exceptions.authorizationError("no indices permissions found");
101103
}
102104
return indicesAccessControl;
103105
}
104106

105-
protected User getUser(){
106-
Authentication authentication = Authentication.getAuthentication(threadContext);
107-
return authentication.getUser();
107+
protected User getUser() {
108+
return Objects.requireNonNull(securityContext.getUser());
108109
}
109110

110111
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
1111
import org.elasticsearch.xpack.core.security.authc.Authentication;
1212
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
13+
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
1314
import org.elasticsearch.xpack.core.watcher.actions.ActionWrapperResult;
1415
import org.elasticsearch.xpack.core.watcher.condition.Condition;
1516
import org.elasticsearch.xpack.core.watcher.history.WatchRecord;
@@ -262,7 +263,7 @@ public static String getUsernameFromWatch(Watch watch) throws IOException {
262263
if (watch != null && watch.status() != null && watch.status().getHeaders() != null) {
263264
String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY);
264265
if (header != null) {
265-
Authentication auth = Authentication.decode(header);
266+
Authentication auth = AuthenticationContextSerializer.decode(header);
266267
return auth.getUser().principal();
267268
}
268269
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
import org.elasticsearch.search.internal.ContextIndexSearcher;
4040
import org.elasticsearch.test.AbstractBuilderTestCase;
4141
import org.elasticsearch.test.IndexSettingsModule;
42+
import org.elasticsearch.xpack.core.security.SecurityContext;
4243
import org.elasticsearch.xpack.core.security.authc.Authentication;
43-
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
44+
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
4445
import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions;
4546
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
4647
import org.elasticsearch.xpack.core.security.user.User;
@@ -69,10 +70,14 @@ public void testDLS() throws Exception {
6970
when(mapperService.simpleMatchToFullName(anyString()))
7071
.then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0]));
7172

72-
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
73+
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
74+
final SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext);
75+
7376
final Authentication authentication = mock(Authentication.class);
7477
when(authentication.getUser()).thenReturn(mock(User.class));
75-
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
78+
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // don't care as long as it's not null
79+
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);
80+
7681
IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(shardId.getIndex(), Settings.EMPTY);
7782
Client client = mock(Client.class);
7883
when(client.settings()).thenReturn(Settings.EMPTY);
@@ -135,7 +140,7 @@ null, null, mapperService, null, null, xContentRegistry(), writableRegistry(),
135140
FieldPermissions(),
136141
DocumentPermissions.filteredBy(singleton(new BytesArray(termQuery))));
137142
SecurityIndexReaderWrapper wrapper = new SecurityIndexReaderWrapper(s -> queryShardContext,
138-
bitsetCache, threadContext, licenseState, scriptService) {
143+
bitsetCache, securityContext, licenseState, scriptService) {
139144

140145
@Override
141146
protected IndicesAccessControl getIndicesAccessControl() {
@@ -173,10 +178,13 @@ public void testDLSWithLimitedPermissions() throws Exception {
173178
when(mapperService.simpleMatchToFullName(anyString()))
174179
.then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0]));
175180

176-
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
181+
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
182+
final SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext);
177183
final Authentication authentication = mock(Authentication.class);
178184
when(authentication.getUser()).thenReturn(mock(User.class));
179-
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
185+
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // don't care as long as it's not null
186+
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);
187+
180188
final boolean noFilteredIndexPermissions = randomBoolean();
181189
boolean restrictiveLimitedIndexPermissions = false;
182190
if (noFilteredIndexPermissions == false) {
@@ -208,7 +216,7 @@ null, null, mapperService, null, null, xContentRegistry(), writableRegistry(),
208216
XPackLicenseState licenseState = mock(XPackLicenseState.class);
209217
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
210218
SecurityIndexReaderWrapper wrapper = new SecurityIndexReaderWrapper(s -> queryShardContext,
211-
bitsetCache, threadContext, licenseState, scriptService) {
219+
bitsetCache, securityContext, licenseState, scriptService) {
212220

213221
@Override
214222
protected IndicesAccessControl getIndicesAccessControl() {

0 commit comments

Comments
 (0)