Skip to content

Commit 4b7160d

Browse files
Ensure authz operation overrides transient authz headers (#61621)
AuthorizationService#authorize uses the thread context to carry the result of the authorisation as transient headers. The listener argument to the `authorize` method must necessarily observe the header values. This PR makes it so that the authorisation transient headers (`_indices_permissions` and `_authz_info`, but NOT `_originating_action_name`) of the child action override the ones of the parent action. Co-authored-by: Tim Vernum [email protected]
1 parent 92fb003 commit 4b7160d

File tree

11 files changed

+307
-101
lines changed

11 files changed

+307
-101
lines changed

server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import java.io.IOException;
3636
import java.nio.charset.StandardCharsets;
37+
import java.util.Collection;
3738
import java.util.Collections;
3839
import java.util.EnumSet;
3940
import java.util.HashMap;
@@ -185,12 +186,41 @@ public StoredContext stashAndMergeHeaders(Map<String, String> headers) {
185186
* @param preserveResponseHeaders if set to <code>true</code> the response headers of the restore thread will be preserved.
186187
*/
187188
public StoredContext newStoredContext(boolean preserveResponseHeaders) {
188-
final ThreadContextStruct context = threadLocal.get();
189-
return () -> {
190-
if (preserveResponseHeaders && threadLocal.get() != context) {
191-
threadLocal.set(context.putResponseHeaders(threadLocal.get().responseHeaders));
189+
return newStoredContext(preserveResponseHeaders, List.of());
190+
}
191+
192+
/**
193+
* Just like {@link #stashContext()} but no default context is set. Instead, the {@code transientHeadersToClear} argument can be used
194+
* to clear specific transient headers in the new context. All headers (with the possible exception of {@code responseHeaders}) are
195+
* restored by closing the returned {@link StoredContext}.
196+
*
197+
* @param preserveResponseHeaders if set to <code>true</code> the response headers of the restore thread will be preserved.
198+
*/
199+
public StoredContext newStoredContext(boolean preserveResponseHeaders, Collection<String> transientHeadersToClear) {
200+
final ThreadContextStruct originalContext = threadLocal.get();
201+
// clear specific transient headers from the current context
202+
Map<String, Object> newTransientHeaders = null;
203+
for (String transientHeaderToClear : transientHeadersToClear) {
204+
if (originalContext.transientHeaders.containsKey(transientHeaderToClear)) {
205+
if (newTransientHeaders == null) {
206+
newTransientHeaders = new HashMap<>(originalContext.transientHeaders);
207+
}
208+
newTransientHeaders.remove(transientHeaderToClear);
209+
}
210+
}
211+
if (newTransientHeaders != null) {
212+
ThreadContextStruct threadContextStruct = new ThreadContextStruct(originalContext.requestHeaders,
213+
originalContext.responseHeaders, newTransientHeaders, originalContext.isSystemContext,
214+
originalContext.warningHeadersSize);
215+
threadLocal.set(threadContextStruct);
216+
}
217+
// this is the context when this method returns
218+
final ThreadContextStruct newContext = threadLocal.get();
219+
return () -> {
220+
if (preserveResponseHeaders && threadLocal.get() != newContext) {
221+
threadLocal.set(originalContext.putResponseHeaders(threadLocal.get().responseHeaders));
192222
} else {
193-
threadLocal.set(context);
223+
threadLocal.set(originalContext);
194224
}
195225
};
196226
}
@@ -504,7 +534,7 @@ private ThreadContextStruct putRequest(String key, String value) {
504534
return new ThreadContextStruct(newRequestHeaders, responseHeaders, transientHeaders, isSystemContext);
505535
}
506536

507-
private static void putSingleHeader(String key, String value, Map<String, String> newHeaders) {
537+
private static <T> void putSingleHeader(String key, T value, Map<String, T> newHeaders) {
508538
if (newHeaders.putIfAbsent(key, value) != null) {
509539
throw new IllegalArgumentException("value for key [" + key + "] already present");
510540
}
@@ -592,12 +622,9 @@ private ThreadContextStruct putResponse(final String key, final String value, fi
592622
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext, newWarningHeaderSize);
593623
}
594624

595-
596625
private ThreadContextStruct putTransient(String key, Object value) {
597626
Map<String, Object> newTransient = new HashMap<>(this.transientHeaders);
598-
if (newTransient.putIfAbsent(key, value) != null) {
599-
throw new IllegalArgumentException("value for key [" + key + "] already present");
600-
}
627+
putSingleHeader(key, value, newTransient);
601628
return new ThreadContextStruct(requestHeaders, responseHeaders, newTransient, isSystemContext);
602629
}
603630

server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.elasticsearch.test.ESTestCase;
2525

2626
import java.io.IOException;
27+
import java.util.Arrays;
2728
import java.util.Collections;
2829
import java.util.HashMap;
2930
import java.util.List;
@@ -56,6 +57,78 @@ public void testStashContext() {
5657
assertEquals("1", threadContext.getHeader("default"));
5758
}
5859

60+
public void testNewContextWithClearedTransients() {
61+
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
62+
threadContext.putTransient("foo", "bar");
63+
threadContext.putTransient("bar", "baz");
64+
threadContext.putHeader("foo", "bar");
65+
threadContext.putHeader("baz", "bar");
66+
threadContext.addResponseHeader("foo", "bar");
67+
threadContext.addResponseHeader("bar", "qux");
68+
69+
// this is missing or null
70+
if (randomBoolean()) {
71+
threadContext.putTransient("acme", null);
72+
}
73+
74+
// foo is the only existing transient header that is cleared
75+
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(false, randomFrom(List.of("foo", "foo"),
76+
List.of("foo"), List.of("foo", "acme")))) {
77+
// only the requested transient header is cleared
78+
assertNull(threadContext.getTransient("foo"));
79+
// missing header is still missing
80+
assertNull(threadContext.getTransient("acme"));
81+
// other headers are preserved
82+
assertEquals("baz", threadContext.getTransient("bar"));
83+
assertEquals("bar", threadContext.getHeader("foo"));
84+
assertEquals("bar", threadContext.getHeader("baz"));
85+
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
86+
assertEquals("qux", threadContext.getResponseHeaders().get("bar").get(0));
87+
88+
// try override stashed header
89+
threadContext.putTransient("foo", "acme");
90+
assertEquals("acme", threadContext.getTransient("foo"));
91+
// add new headers
92+
threadContext.putTransient("baz", "bar");
93+
threadContext.putHeader("bar", "baz");
94+
threadContext.addResponseHeader("baz", "bar");
95+
threadContext.addResponseHeader("foo", "baz");
96+
}
97+
98+
// original is restored (it is not overridden)
99+
assertEquals("bar", threadContext.getTransient("foo"));
100+
// headers added inside the stash are NOT preserved
101+
assertNull(threadContext.getTransient("baz"));
102+
assertNull(threadContext.getHeader("bar"));
103+
assertNull(threadContext.getResponseHeaders().get("baz"));
104+
// original headers are restored
105+
assertEquals("bar", threadContext.getHeader("foo"));
106+
assertEquals("bar", threadContext.getHeader("baz"));
107+
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
108+
assertEquals(1, threadContext.getResponseHeaders().get("foo").size());
109+
assertEquals("qux", threadContext.getResponseHeaders().get("bar").get(0));
110+
111+
// test stashed missing header stays missing
112+
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(randomBoolean(), randomFrom(Arrays.asList("acme", "acme"),
113+
Arrays.asList("acme")))) {
114+
assertNull(threadContext.getTransient("acme"));
115+
threadContext.putTransient("acme", "foo");
116+
}
117+
assertNull(threadContext.getTransient("acme"));
118+
119+
// test preserved response headers
120+
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(true, randomFrom(List.of("foo", "foo"),
121+
List.of("foo"), List.of("foo", "acme")))) {
122+
threadContext.addResponseHeader("baz", "bar");
123+
threadContext.addResponseHeader("foo", "baz");
124+
}
125+
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
126+
assertEquals("baz", threadContext.getResponseHeaders().get("foo").get(1));
127+
assertEquals(2, threadContext.getResponseHeaders().get("foo").size());
128+
assertEquals("bar", threadContext.getResponseHeaders().get("baz").get(0));
129+
assertEquals(1, threadContext.getResponseHeaders().get("baz").size());
130+
}
131+
59132
public void testStashWithOrigin() {
60133
final String origin = randomAlphaOfLengthBetween(4, 16);
61134
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55
*/
66
package org.elasticsearch.xpack.core.security.authz;
77

8+
import java.util.Collection;
9+
import java.util.List;
10+
811
public final class AuthorizationServiceField {
12+
913
public static final String INDICES_PERMISSIONS_KEY = "_indices_permissions";
14+
public static final String ORIGINATING_ACTION_KEY = "_originating_action_name";
15+
public static final String AUTHORIZATION_INFO_KEY = "_authz_info";
16+
17+
// Most often, transient authorisation headers are scoped (i.e. set, read and cleared) for the authorisation and execution
18+
// of individual actions (i.e. there is a different scope between the parent and the child actions)
19+
public static final Collection<String> ACTION_SCOPE_AUTHORIZATION_KEYS = List.of(INDICES_PERMISSIONS_KEY, AUTHORIZATION_INFO_KEY);
1020

1121
private AuthorizationServiceField() {}
1222
}

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
import java.util.Collections;
4141

4242
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
43-
import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY;
44-
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
43+
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
44+
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
4545
import static org.elasticsearch.xpack.security.authz.AuthorizationServiceTests.authzInfoRoles;
4646
import static org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener.ensureAuthenticatedUserIsSame;
4747
import static org.hamcrest.Matchers.is;

0 commit comments

Comments
 (0)