Skip to content

Commit 2386a8c

Browse files
authored
[Test] Add RCS2 tests for ML anomaly detection jobs (#95263)
This PR adds smoke tests for ML anomaly detection jobs with RCS 2. It also explicit tests that data frame analytics currently does not support remote indices.
1 parent 0b4b741 commit 2386a8c

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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.client.ResponseException;
14+
import org.elasticsearch.core.Strings;
15+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
16+
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
17+
import org.elasticsearch.test.cluster.util.resource.Resource;
18+
import org.elasticsearch.test.rest.ObjectPath;
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.Map;
25+
import java.util.concurrent.atomic.AtomicReference;
26+
27+
import static org.hamcrest.Matchers.contains;
28+
import static org.hamcrest.Matchers.containsString;
29+
import static org.hamcrest.Matchers.equalTo;
30+
31+
public class RemoteClusterSecurityMlIT extends AbstractRemoteClusterSecurityTestCase {
32+
33+
private static final AtomicReference<Map<String, Object>> API_KEY_MAP_REF = new AtomicReference<>();
34+
private static final String REMOTE_ML_USER = "remote_ml_user";
35+
36+
static {
37+
fulfillingCluster = ElasticsearchCluster.local()
38+
.name("fulfilling-cluster")
39+
.apply(commonClusterConfig)
40+
.distribution(DistributionType.DEFAULT)
41+
.setting("remote_cluster_server.enabled", "true")
42+
.setting("remote_cluster.port", "0")
43+
.setting("xpack.security.remote_cluster_server.ssl.enabled", "true")
44+
.setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key")
45+
.setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt")
46+
.setting("xpack.security.authc.token.enabled", "true")
47+
.keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password")
48+
.build();
49+
50+
queryCluster = ElasticsearchCluster.local()
51+
.name("query-cluster")
52+
.apply(commonClusterConfig)
53+
.distribution(DistributionType.DEFAULT)
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+
.setting("xpack.security.authc.token.enabled", "true")
57+
.keystore("cluster.remote.my_remote_cluster.credentials", () -> {
58+
API_KEY_MAP_REF.compareAndSet(null, createCrossClusterAccessApiKey("""
59+
{
60+
"role": {
61+
"cluster": ["cross_cluster_access"],
62+
"index": [
63+
{
64+
"names": ["shared-airline-data"],
65+
"privileges": ["read", "read_cross_cluster", "view_index_metadata"]
66+
}
67+
]
68+
}
69+
}"""));
70+
return (String) API_KEY_MAP_REF.get().get("encoded");
71+
})
72+
.rolesFile(Resource.fromClasspath("roles.yml"))
73+
.user(REMOTE_ML_USER, PASS.toString(), "ml_jobs_shared_airline_data")
74+
.build();
75+
}
76+
77+
@ClassRule
78+
// Use a RuleChain to ensure that fulfilling cluster is started before query cluster
79+
public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster);
80+
81+
public void testAnomalyDetectionAndDatafeed() throws Exception {
82+
configureRemoteCluster();
83+
84+
// Fulfilling cluster
85+
{
86+
final Request createIndexRequest1 = new Request("PUT", "shared-airline-data");
87+
createIndexRequest1.setJsonEntity("""
88+
{
89+
"mappings": {
90+
"properties": {
91+
"time": { "type": "date" },
92+
"airline": { "type": "keyword" },
93+
"responsetime": { "type": "float" },
94+
"event_rate": { "type": "integer" }
95+
}
96+
}
97+
}""");
98+
assertOK(performRequestAgainstFulfillingCluster(createIndexRequest1));
99+
100+
final Request bulkRequest1 = new Request("POST", "/_bulk?refresh=true");
101+
bulkRequest1.setJsonEntity(Strings.format("""
102+
{"index": {"_index": "shared-airline-data", "_id": "1"}}
103+
{"airline": "foo", "responsetime": 1.0, "time" : "2017-02-18T00:00:00Z"}
104+
{"index": {"_index": "shared-airline-data", "_id": "2"}}
105+
{"airline": "foo", "responsetime": 1.0, "time" : "2017-02-18T00:30:00Z"}
106+
{"index": {"_index": "shared-airline-data", "_id": "3"}}
107+
{"airline": "foo", "responsetime": 42.0, "time" : "2017-02-18T01:00:00Z"}
108+
{"index": {"_index": "shared-airline-data", "_id": "4"}}
109+
{"airline": "foo", "responsetime": 42.0, "time" : "2017-02-18T01:01:00Z"}
110+
"""));
111+
assertOK(performRequestAgainstFulfillingCluster(bulkRequest1));
112+
113+
// Just some index that is not accessible remotely
114+
assertOK(performRequestAgainstFulfillingCluster(new Request("PUT", "private-airline-data")));
115+
}
116+
117+
// Query cluster
118+
{
119+
// A working anomaly detection job
120+
final var putMlJobRequest1 = new Request("PUT", "/_ml/anomaly_detectors/remote-detection-job");
121+
putMlJobRequest1.setJsonEntity("""
122+
{
123+
"description":"Analysis of response time by airline",
124+
"analysis_config" : {
125+
"bucket_span": "1h",
126+
"detectors": [{"function":"sum","field_name":"responsetime","by_field_name":"airline"}]
127+
},
128+
"data_description" : {
129+
"format":"xcontent"
130+
},
131+
"datafeed_config": {
132+
"indexes":["my_remote_cluster:shared-airline-data"]
133+
}
134+
}
135+
""");
136+
final ObjectPath putMlJobResponse = assertOKAndCreateObjectPath(performRequestWithRemoteMlUser(putMlJobRequest1));
137+
assertThat(putMlJobResponse.evaluate("job_id"), equalTo("remote-detection-job"));
138+
assertThat(putMlJobResponse.evaluate("datafeed_config.job_id"), equalTo("remote-detection-job"));
139+
assertThat(putMlJobResponse.evaluate("datafeed_config.datafeed_id"), equalTo("remote-detection-job"));
140+
assertThat(putMlJobResponse.evaluate("datafeed_config.authorization.roles"), contains("ml_jobs_shared_airline_data"));
141+
142+
assertOK(performRequestWithRemoteMlUser(new Request("POST", "/_ml/anomaly_detectors/remote-detection-job/_open")));
143+
assertOK(performRequestWithRemoteMlUser(new Request("POST", "/_ml/datafeeds/remote-detection-job/_start")));
144+
145+
final ObjectPath previewDatafeedResponse = assertOKAndCreateObjectPath(
146+
performRequestWithRemoteMlUser(new Request("GET", "/_ml/datafeeds/remote-detection-job/_preview"))
147+
);
148+
assertThat(previewDatafeedResponse.evaluate("0.time"), equalTo(1487376000000L));
149+
assertThat(previewDatafeedResponse.evaluate("1.time"), equalTo(1487377800000L));
150+
assertThat(previewDatafeedResponse.evaluate("2.time"), equalTo(1487379600000L));
151+
assertThat(previewDatafeedResponse.evaluate("3.time"), equalTo(1487379660000L));
152+
153+
// A failure case
154+
final var putMlJobRequest2 = new Request("PUT", "/_ml/anomaly_detectors/invalid");
155+
putMlJobRequest2.setJsonEntity("""
156+
{
157+
"analysis_config" : {
158+
"bucket_span": "1h",
159+
"detectors": [{"function":"sum","field_name":"responsetime","by_field_name":"airline"}]
160+
},
161+
"data_description" : {
162+
"format":"xcontent"
163+
},
164+
"datafeed_config": {
165+
"indexes":["my_remote_cluster:private-airline-data"]
166+
}
167+
}
168+
""");
169+
final ObjectPath putMlJobResponse2 = assertOKAndCreateObjectPath(performRequestWithRemoteMlUser(putMlJobRequest2));
170+
assertThat(putMlJobResponse2.evaluate("job_id"), equalTo("invalid"));
171+
assertThat(putMlJobResponse2.evaluate("datafeed_config.job_id"), equalTo("invalid"));
172+
assertThat(putMlJobResponse2.evaluate("datafeed_config.datafeed_id"), equalTo("invalid"));
173+
174+
assertOK(performRequestWithRemoteMlUser(new Request("POST", "/_ml/anomaly_detectors/invalid/_open")));
175+
176+
final ResponseException startDatafeedException = expectThrows(
177+
ResponseException.class,
178+
() -> performRequestWithRemoteMlUser(new Request("POST", "/_ml/datafeeds/invalid/_start"))
179+
);
180+
assertThat(startDatafeedException.getResponse().getStatusLine().getStatusCode(), equalTo(403));
181+
assertThat(startDatafeedException.getMessage(), containsString("unauthorized for user [" + REMOTE_ML_USER + "]"));
182+
183+
final ResponseException previewDatafeedException = expectThrows(
184+
ResponseException.class,
185+
() -> performRequestWithRemoteMlUser(new Request("GET", "/_ml/datafeeds/invalid/_preview"))
186+
);
187+
assertThat(previewDatafeedException.getResponse().getStatusLine().getStatusCode(), equalTo(403));
188+
assertThat(previewDatafeedException.getMessage(), containsString("unauthorized for user [" + REMOTE_ML_USER + "]"));
189+
}
190+
}
191+
192+
public void testDataframeAnalyticsNotSupportForRemoteIndices() {
193+
final Request putDataframeAnalytics = new Request("PUT", "/_ml/data_frame/analytics/invalid");
194+
putDataframeAnalytics.setJsonEntity("""
195+
{
196+
"source": {
197+
"index": "my_remote_cluster:shared-airline-data"
198+
},
199+
"dest": {
200+
"index": "data-frame-analytics-dest"
201+
},
202+
"analysis": {"outlier_detection":{}}
203+
}
204+
""");
205+
final ResponseException e = expectThrows(ResponseException.class, () -> performRequestWithRemoteMlUser(putDataframeAnalytics));
206+
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400));
207+
assertThat(e.getMessage(), containsString("remote source indices are not supported"));
208+
}
209+
210+
private Response performRequestWithRemoteMlUser(final Request request) throws IOException {
211+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(REMOTE_ML_USER, PASS)));
212+
return client().performRequest(request);
213+
}
214+
}

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,13 @@ transform_remote_shared_index:
1818
- names: [ 'shared-transform-index' ]
1919
privileges: [ 'read', 'read_cross_cluster', 'view_index_metadata' ]
2020
clusters: [ 'my_*' ]
21+
22+
ml_jobs_shared_airline_data:
23+
cluster: [ 'manage_ml' ]
24+
indices:
25+
- names: [ 'data-frame-analytics-dest' ]
26+
privileges: [ 'all' ]
27+
remote_indices:
28+
- names: [ 'shared-airline-data' ]
29+
privileges: [ 'read', 'read_cross_cluster', 'view_index_metadata' ]
30+
clusters: [ 'my_*' ]

0 commit comments

Comments
 (0)