Skip to content

Commit 88f9b54

Browse files
gmjehovichelasticsearchmachinen1v0lg
authored
Correct slow log user for RCS 2.0 (#130140)
* Show Original User in Slow Logs for RCS 2.0 * fix commenting * Update docs/changelog/130140.yaml * [CI] Auto commit changes from spotless * Add Integration Tests for Cross Cluster Search Slow Logs * [CI] Auto commit changes from spotless * integ test bugfix * Apply suggestions from code review Clean up inline comments Co-authored-by: Nikolaj Volgushev <n1v0lg@users.noreply.github.com> * [CI] Auto commit changes from spotless * Refactor SlowLog unit tests to use AuthenticationTestHelper * Refactor populateAuthContextMap to create auth context map * Remove javadocs * Clean up comments --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co> Co-authored-by: Nikolaj Volgushev <n1v0lg@users.noreply.github.com>
1 parent 48cb0ef commit 88f9b54

File tree

4 files changed

+686
-21
lines changed

4 files changed

+686
-21
lines changed

docs/changelog/130140.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 130140
2+
summary: Correct slow log user for RCS 2.0
3+
area: Authentication
4+
type: enhancement
5+
issues: []
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.remotecluster;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.client.RequestOptions;
12+
import org.elasticsearch.client.Response;
13+
import org.elasticsearch.common.io.Streams;
14+
import org.elasticsearch.common.xcontent.XContentHelper;
15+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
16+
import org.elasticsearch.test.cluster.LogType;
17+
import org.elasticsearch.test.rest.ObjectPath;
18+
import org.elasticsearch.xcontent.XContentType;
19+
import org.junit.ClassRule;
20+
import org.junit.rules.RuleChain;
21+
import org.junit.rules.TestRule;
22+
23+
import java.io.IOException;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
29+
import static org.hamcrest.Matchers.empty;
30+
import static org.hamcrest.Matchers.equalTo;
31+
import static org.hamcrest.Matchers.hasKey;
32+
import static org.hamcrest.Matchers.not;
33+
import static org.hamcrest.Matchers.notNullValue;
34+
35+
public class RemoteClusterSecuritySlowLogRestIT extends AbstractRemoteClusterSecurityTestCase {
36+
37+
private static final AtomicReference<Map<String, Object>> API_KEY_MAP_REF = new AtomicReference<>();
38+
39+
static {
40+
fulfillingCluster = ElasticsearchCluster.local()
41+
.name("fulfilling-cluster")
42+
.apply(commonClusterConfig)
43+
.setting("remote_cluster_server.enabled", "true")
44+
.setting("remote_cluster.port", "0")
45+
.setting("xpack.security.remote_cluster_server.ssl.enabled", "true")
46+
.setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key")
47+
.setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt")
48+
.keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password")
49+
.build();
50+
51+
queryCluster = ElasticsearchCluster.local()
52+
.name("query-cluster")
53+
.apply(commonClusterConfig)
54+
.setting("xpack.security.remote_cluster_client.ssl.enabled", "true")
55+
.setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt")
56+
.keystore("cluster.remote.my_remote_cluster.credentials", () -> {
57+
if (API_KEY_MAP_REF.get() == null) {
58+
final Map<String, Object> apiKeyMap = createCrossClusterAccessApiKey("""
59+
{
60+
"search": [
61+
{
62+
"names": ["slow_log_*", "run_as_*"]
63+
}
64+
]
65+
}""");
66+
API_KEY_MAP_REF.set(apiKeyMap);
67+
}
68+
return (String) API_KEY_MAP_REF.get().get("encoded");
69+
})
70+
.build();
71+
}
72+
73+
@ClassRule
74+
public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster);
75+
76+
public void testCrossClusterSlowLogAuthenticationContext() throws Exception {
77+
configureRemoteCluster();
78+
79+
// Fulfilling cluster setup
80+
{
81+
// Create an index with slow log settings enabled
82+
final Request createIndexRequest = new Request("PUT", "/slow_log_test");
83+
createIndexRequest.setJsonEntity("""
84+
{
85+
"settings": {
86+
"index.search.slowlog.threshold.query.trace": "0ms",
87+
"index.search.slowlog.include.user": true
88+
},
89+
"mappings": {
90+
"properties": {
91+
"content": { "type": "text" },
92+
"timestamp": { "type": "date" }
93+
}
94+
}
95+
}""");
96+
assertOK(performRequestAgainstFulfillingCluster(createIndexRequest));
97+
98+
// test documents
99+
final Request bulkRequest = new Request("POST", "/_bulk?refresh=true");
100+
bulkRequest.setJsonEntity("""
101+
{ "index": { "_index": "slow_log_test" } }
102+
{ "content": "test content for slow log", "timestamp": "2024-01-01T10:00:00Z" }
103+
{ "index": { "_index": "slow_log_test" } }
104+
{ "content": "another test document", "timestamp": "2024-01-01T11:00:00Z" }
105+
""");
106+
assertOK(performRequestAgainstFulfillingCluster(bulkRequest));
107+
}
108+
109+
// Query cluster setup
110+
{
111+
// Create user role with remote cluster privileges
112+
final var putRoleRequest = new Request("PUT", "/_security/role/slow_log_remote_role");
113+
putRoleRequest.setJsonEntity("""
114+
{
115+
"description": "Role for testing slow log auth context with cross-cluster access",
116+
"cluster": ["manage_own_api_key"],
117+
"remote_indices": [
118+
{
119+
"names": ["slow_log_*"],
120+
"privileges": ["read", "read_cross_cluster"],
121+
"clusters": ["my_remote_cluster"]
122+
}
123+
]
124+
}""");
125+
assertOK(adminClient().performRequest(putRoleRequest));
126+
127+
// Create test user
128+
final var putUserRequest = new Request("PUT", "/_security/user/slow_log_test_user");
129+
putUserRequest.setJsonEntity("""
130+
{
131+
"password": "x-pack-test-password",
132+
"roles": ["slow_log_remote_role"],
133+
"full_name": "Slow Log Test User"
134+
}""");
135+
assertOK(adminClient().performRequest(putUserRequest));
136+
137+
// Create API key for the test user
138+
final var createApiKeyRequest = new Request("PUT", "/_security/api_key");
139+
createApiKeyRequest.setJsonEntity("""
140+
{
141+
"name": "slow_log_test_api_key",
142+
"role_descriptors": {
143+
"slow_log_access": {
144+
"remote_indices": [
145+
{
146+
"names": ["slow_log_*"],
147+
"privileges": ["read", "read_cross_cluster"],
148+
"clusters": ["my_remote_cluster"]
149+
}
150+
]
151+
}
152+
}
153+
}""");
154+
final var createApiKeyResponse = performRequestWithSlowLogTestUser(createApiKeyRequest);
155+
assertOK(createApiKeyResponse);
156+
157+
var createApiKeyResponsePath = ObjectPath.createFromResponse(createApiKeyResponse);
158+
final String apiKeyEncoded = createApiKeyResponsePath.evaluate("encoded");
159+
final String apiKeyId = createApiKeyResponsePath.evaluate("id");
160+
assertThat(apiKeyEncoded, notNullValue());
161+
assertThat(apiKeyId, notNullValue());
162+
163+
// Perform cross-cluster search that should generate slow log entries
164+
final var searchRequest = new Request("GET", "/my_remote_cluster:slow_log_test/_search");
165+
searchRequest.setJsonEntity("""
166+
{
167+
"query": {
168+
"match": {
169+
"content": "test"
170+
}
171+
},
172+
"sort": [
173+
{ "timestamp": { "order": "desc" } }
174+
]
175+
}""");
176+
177+
// Execute search with API key authentication
178+
final Response searchResponse = performRequestWithApiKey(searchRequest, apiKeyEncoded);
179+
assertOK(searchResponse);
180+
181+
// Verify slow log contains correct authentication context from the original user
182+
Map<String, Object> expectedAuthContext = Map.of(
183+
"user.name",
184+
"slow_log_test_user",
185+
"user.realm",
186+
"_es_api_key",
187+
"user.full_name",
188+
"Slow Log Test User",
189+
"auth.type",
190+
"API_KEY",
191+
"apikey.id",
192+
apiKeyId,
193+
"apikey.name",
194+
"slow_log_test_api_key"
195+
);
196+
197+
verifySlowLogAuthenticationContext(expectedAuthContext);
198+
}
199+
}
200+
201+
public void testRunAsUserInCrossClusterSlowLog() throws Exception {
202+
configureRemoteCluster();
203+
204+
// Fulfilling cluster setup
205+
{
206+
final Request createIndexRequest = new Request("PUT", "/run_as_test");
207+
createIndexRequest.setJsonEntity("""
208+
{
209+
"settings": {
210+
"index.search.slowlog.threshold.query.trace": "0ms",
211+
"index.search.slowlog.include.user": true
212+
},
213+
"mappings": {
214+
"properties": {
215+
"data": { "type": "text" }
216+
}
217+
}
218+
}""");
219+
assertOK(performRequestAgainstFulfillingCluster(createIndexRequest));
220+
221+
final Request indexRequest = new Request("POST", "/run_as_test/_doc?refresh=true");
222+
indexRequest.setJsonEntity("""
223+
{ "data": "run as test data" }""");
224+
assertOK(performRequestAgainstFulfillingCluster(indexRequest));
225+
}
226+
227+
// Query cluster setup
228+
{
229+
// Create role that allows run-as and remote access
230+
final var putRunAsRoleRequest = new Request("PUT", "/_security/role/run_as_remote_role");
231+
putRunAsRoleRequest.setJsonEntity("""
232+
{
233+
"description": "Role that can run as other users and access remote clusters",
234+
"cluster": ["manage_own_api_key"],
235+
"run_as": ["target_user"],
236+
"remote_indices": [
237+
{
238+
"names": ["run_as_*"],
239+
"privileges": ["read", "read_cross_cluster"],
240+
"clusters": ["my_remote_cluster"]
241+
}
242+
]
243+
}""");
244+
assertOK(adminClient().performRequest(putRunAsRoleRequest));
245+
246+
// Create the run-as user
247+
final var putRunAsUserRequest = new Request("PUT", "/_security/user/run_as_user");
248+
putRunAsUserRequest.setJsonEntity("""
249+
{
250+
"password": "x-pack-test-password",
251+
"roles": ["run_as_remote_role"],
252+
"full_name": "Run As User"
253+
}""");
254+
assertOK(adminClient().performRequest(putRunAsUserRequest));
255+
256+
// Create target user (who will be run as)
257+
final var putTargetUserRequest = new Request("PUT", "/_security/user/target_user");
258+
putTargetUserRequest.setJsonEntity("""
259+
{
260+
"password": "x-pack-test-password",
261+
"roles": ["run_as_remote_role"],
262+
"full_name": "Target User"
263+
}""");
264+
assertOK(adminClient().performRequest(putTargetUserRequest));
265+
266+
// Perform search with run-as header
267+
final var runAsSearchRequest = new Request("GET", "/my_remote_cluster:run_as_test/_search");
268+
runAsSearchRequest.setJsonEntity("""
269+
{
270+
"query": { "match_all": {} }
271+
}""");
272+
273+
// Add both authentication and run-as headers
274+
runAsSearchRequest.setOptions(
275+
RequestOptions.DEFAULT.toBuilder()
276+
.addHeader("Authorization", basicAuthHeaderValue("run_as_user", PASS))
277+
.addHeader("es-security-runas-user", "target_user")
278+
);
279+
280+
final Response runAsResponse = client().performRequest(runAsSearchRequest);
281+
assertOK(runAsResponse);
282+
283+
Map<String, Object> expectedRunAsAuthContext = Map.of(
284+
"user.name",
285+
"run_as_user",
286+
"user.realm",
287+
"default_native",
288+
"user.full_name",
289+
"Run As User",
290+
"user.effective.name",
291+
"target_user",
292+
"user.effective.realm",
293+
"default_native",
294+
"user.effective.full_name",
295+
"Target User",
296+
"auth.type",
297+
"REALM"
298+
);
299+
300+
verifySlowLogAuthenticationContext(expectedRunAsAuthContext);
301+
}
302+
}
303+
304+
private void verifySlowLogAuthenticationContext(Map<String, Object> expectedAuthContext) throws Exception {
305+
assertBusy(() -> {
306+
try (var slowLog = fulfillingCluster.getNodeLog(0, LogType.SEARCH_SLOW)) {
307+
final List<String> lines = Streams.readAllLines(slowLog);
308+
assertThat(lines, not(empty()));
309+
310+
// Get the most recent slow log entry
311+
String lastLogLine = lines.get(lines.size() - 1);
312+
Map<String, Object> logEntry = XContentHelper.convertToMap(XContentType.JSON.xContent(), lastLogLine, true);
313+
314+
for (Map.Entry<String, Object> expectedEntry : expectedAuthContext.entrySet()) {
315+
assertThat(
316+
"Slow log should contain " + expectedEntry.getKey() + " with value " + expectedEntry.getValue(),
317+
logEntry,
318+
hasKey(expectedEntry.getKey())
319+
);
320+
assertThat(
321+
"Slow log " + expectedEntry.getKey() + " should match expected value",
322+
logEntry.get(expectedEntry.getKey()),
323+
equalTo(expectedEntry.getValue())
324+
);
325+
}
326+
}
327+
}, 10, TimeUnit.SECONDS);
328+
}
329+
330+
private Response performRequestWithSlowLogTestUser(final Request request) throws IOException {
331+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue("slow_log_test_user", PASS)));
332+
return client().performRequest(request);
333+
}
334+
335+
private Response performRequestWithApiKey(final Request request, final String encoded) throws IOException {
336+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + encoded));
337+
return client().performRequest(request);
338+
}
339+
}

0 commit comments

Comments
 (0)