Skip to content

Commit 3228058

Browse files
authored
Adding manage_dlm privilege (#95512)
This adds a new index privilege called `manage_dlm`. The `manage_dlm` has permission to perform all DLM actions on an index, including put and delete. It also adds the ability to call DLM get and explain to the `view_index_metadata` existing index privilege.
1 parent b8a5704 commit 3228058

File tree

14 files changed

+278
-2
lines changed

14 files changed

+278
-2
lines changed

docs/changelog/95512.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 95512
2+
summary: Adding `manage_dlm` index privilege and expanding `view_index_metadata` for access to data lifecycle APIs
3+
area: DLM
4+
type: enhancement
5+
issues: []

docs/reference/dlm/apis/delete-lifecycle.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ experimental::[]
88

99
Deletes the lifecycle from a set of data streams.
1010

11+
[[delete-lifecycle-api-prereqs]]
12+
==== {api-prereq-title}
13+
14+
* If the {es} {security-features} are enabled, you must have the `manage_dlm` index privilege or higher to
15+
use this API. For more information, see <<security-privileges>>.
16+
1117
[[dlm-delete-lifecycle-request]]
1218
==== {api-request-title}
1319

docs/reference/dlm/apis/explain-data-lifecycle.asciidoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ experimental::[]
88

99
Retrieves the current data lifecycle status for one or more data stream backing indices.
1010

11+
[[explain-lifecycle-api-prereqs]]
12+
==== {api-prereq-title}
13+
14+
* Nit: would rephrase as:
15+
16+
If the {es} {security-features} are enabled, you must have at least the `manage_dlm` index privilege or
17+
`view_index_metadata` index privilege to use this API. For more information, see <<security-privileges>>.
18+
1119
[[dlm-explain-lifecycle-request]]
1220
==== {api-request-title}
1321

docs/reference/dlm/apis/get-lifecycle.asciidoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ experimental::[]
88

99
Gets the lifecycle of a set of data streams.
1010

11+
[[get-lifecycle-api-prereqs]]
12+
==== {api-prereq-title}
13+
14+
* If the {es} {security-features} are enabled, you must have at least one of the `manage`
15+
<<privileges-list-indices,index privilege>>, the `manage_dlm` index privilege, or the
16+
`view_index_metadata` privilege to use this API. For more information, see <<security-privileges>>.
17+
1118
[[dlm-get-lifecycle-request]]
1219
==== {api-request-title}
1320

docs/reference/dlm/apis/put-lifecycle.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ experimental::[]
88

99
Configures the data lifecycle for the targeted data streams.
1010

11+
[[put-lifecycle-api-prereqs]]
12+
==== {api-prereq-title}
13+
14+
If the {es} {security-features} are enabled, you must have the `manage_dlm` index privilege or higher to use this API.
15+
For more information, see <<security-privileges>>.
16+
1117
[[dlm-put-lifecycle-request]]
1218
==== {api-request-title}
1319

modules/dlm/qa/build.gradle

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import org.elasticsearch.gradle.Version
2+
3+
apply plugin: 'elasticsearch.legacy-java-rest-test'
4+
apply plugin: 'elasticsearch.authenticated-testclusters'
5+
6+
dependencies {
7+
javaRestTestImplementation project(":client:rest-high-level")
8+
}
9+
10+
testClusters.configureEach {
11+
testDistribution = 'DEFAULT'
12+
setting 'xpack.watcher.enabled', 'false'
13+
setting 'xpack.ml.enabled', 'false'
14+
setting 'xpack.license.self_generated.type', 'trial'
15+
rolesFile file('roles.yml')
16+
user username: "test_dlm", password: "x-pack-test-password", role: "manage_dlm"
17+
user username: "test_non_privileged", password: "x-pack-test-password", role: "not_privileged"
18+
requiresFeature 'es.dlm_feature_flag_enabled', Version.fromString("8.9.0")
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
manage_dlm:
2+
cluster:
3+
- monitor
4+
indices:
5+
- names: [ 'dlm-*' ]
6+
privileges:
7+
- read
8+
- write
9+
- manage_dlm
10+
not_privileged:
11+
cluster:
12+
- monitor
13+
indices:
14+
- names: [ 'dlm-*' ]
15+
privileges:
16+
- read
17+
- write
18+
- view_index_metadata
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.dlm;
10+
11+
import org.apache.http.HttpHost;
12+
import org.elasticsearch.client.Request;
13+
import org.elasticsearch.client.Response;
14+
import org.elasticsearch.client.ResponseException;
15+
import org.elasticsearch.client.RestClient;
16+
import org.elasticsearch.common.Strings;
17+
import org.elasticsearch.common.settings.SecureString;
18+
import org.elasticsearch.common.settings.Settings;
19+
import org.elasticsearch.common.util.concurrent.ThreadContext;
20+
import org.elasticsearch.rest.RestStatus;
21+
import org.elasticsearch.test.rest.ESRestTestCase;
22+
import org.elasticsearch.test.rest.ObjectPath;
23+
24+
import java.io.IOException;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
import static org.hamcrest.Matchers.equalTo;
29+
30+
public class PermissionsIT extends ESRestTestCase {
31+
32+
@Override
33+
protected Settings restClientSettings() {
34+
// Note: This user is defined in build.gradle, and assigned the role "manage_dlm". That role is defined in roles.yml.
35+
String token = basicAuthHeaderValue("test_dlm", new SecureString("x-pack-test-password".toCharArray()));
36+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
37+
}
38+
39+
@Override
40+
protected Settings restAdminSettings() {
41+
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
42+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
43+
}
44+
45+
private Settings restUnprivilegedClientSettings() {
46+
// Note: This user is defined in build.gradle, and assigned the role "not_privileged". That role is defined in roles.yml.
47+
String token = basicAuthHeaderValue("test_non_privileged", new SecureString("x-pack-test-password".toCharArray()));
48+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
49+
}
50+
51+
@SuppressWarnings("unchecked")
52+
public void testManageDLM() throws Exception {
53+
{
54+
/*
55+
* This test checks that a user with the "manage_dlm" index privilege on "dlm-*" data streams can delete and put a lifecycle
56+
* on the "dlm-test" data stream, while a user with who does not have that privilege (but does have all of the other same
57+
* "dlm-*" privileges) cannot delete or put a lifecycle on that datastream.
58+
*/
59+
String dataStreamName = "dlm-test"; // Needs to match the pattern of the names in roles.yml
60+
createDataStreamAsAdmin(dataStreamName);
61+
Response getDatastreamRepsonse = adminClient().performRequest(new Request("GET", "/_data_stream/" + dataStreamName));
62+
final List<Map<String, Object>> nodes = ObjectPath.createFromResponse(getDatastreamRepsonse).evaluate("data_streams");
63+
String index = (String) ((List<Map<String, Object>>) nodes.get(0).get("indices")).get(0).get("index_name");
64+
65+
Request explainLifecycleRequest = new Request("GET", "/" + randomFrom("_all", "*", index) + "/_lifecycle/explain");
66+
Request getLifecycleRequest = new Request("GET", "_data_stream/" + randomFrom("_all", "*", dataStreamName) + "/_lifecycle");
67+
Request deleteLifecycleRequest = new Request(
68+
"DELETE",
69+
"_data_stream/" + randomFrom("_all", "*", dataStreamName) + "/_lifecycle"
70+
);
71+
Request putLifecycleRequest = new Request("PUT", "_data_stream/" + randomFrom("_all", "*", dataStreamName) + "/_lifecycle");
72+
putLifecycleRequest.setJsonEntity("{}");
73+
74+
makeRequest(client(), explainLifecycleRequest, true);
75+
makeRequest(client(), getLifecycleRequest, true);
76+
makeRequest(client(), deleteLifecycleRequest, true);
77+
makeRequest(client(), putLifecycleRequest, true);
78+
79+
try (
80+
RestClient nonDlmManagerClient = buildClient(restUnprivilegedClientSettings(), getClusterHosts().toArray(new HttpHost[0]))
81+
) {
82+
makeRequest(nonDlmManagerClient, explainLifecycleRequest, true);
83+
makeRequest(nonDlmManagerClient, getLifecycleRequest, true);
84+
makeRequest(nonDlmManagerClient, deleteLifecycleRequest, false);
85+
makeRequest(nonDlmManagerClient, putLifecycleRequest, false);
86+
}
87+
}
88+
{
89+
// Now test that the user who has the manage_dlm privilege on dlm-* data streams cannot manage other data streams:
90+
String otherDataStreamName = "other-dlm-test";
91+
createDataStreamAsAdmin(otherDataStreamName);
92+
Response getOtherDataStreamResponse = adminClient().performRequest(new Request("GET", "/_data_stream/" + otherDataStreamName));
93+
final List<Map<String, Object>> otherNodes = ObjectPath.createFromResponse(getOtherDataStreamResponse).evaluate("data_streams");
94+
String otherIndex = (String) ((List<Map<String, Object>>) otherNodes.get(0).get("indices")).get(0).get("index_name");
95+
Request putOtherLifecycleRequest = new Request("PUT", "_data_stream/" + otherDataStreamName + "/_lifecycle");
96+
putOtherLifecycleRequest.setJsonEntity("{}");
97+
makeRequest(client(), new Request("GET", "/" + otherIndex + "/_lifecycle/explain"), false);
98+
makeRequest(client(), new Request("GET", "_data_stream/" + otherDataStreamName + "/_lifecycle"), false);
99+
makeRequest(client(), new Request("DELETE", "_data_stream/" + otherDataStreamName + "/_lifecycle"), false);
100+
makeRequest(client(), putOtherLifecycleRequest, false);
101+
}
102+
}
103+
104+
/*
105+
* This makes the given request with the given client. It asserts a 200 response if expectSuccess is true, and asserts an exception
106+
* with a 403 response if expectStatus is false.
107+
*/
108+
private void makeRequest(RestClient client, Request request, boolean expectSuccess) throws IOException {
109+
if (expectSuccess) {
110+
Response response = client.performRequest(request);
111+
assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
112+
} else {
113+
ResponseException exception = expectThrows(ResponseException.class, () -> client.performRequest(request));
114+
assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus()));
115+
}
116+
}
117+
118+
private void createDataStreamAsAdmin(String name) throws IOException {
119+
String mappingsTemplateName = name + "_mappings";
120+
Request mappingsRequest = new Request("PUT", "/_component_template/" + mappingsTemplateName);
121+
mappingsRequest.setJsonEntity("""
122+
{
123+
"template": {
124+
"mappings": {
125+
"properties": {
126+
"@timestamp": {
127+
"type": "date",
128+
"format": "date_optional_time||epoch_millis"
129+
},
130+
"message": {
131+
"type": "wildcard"
132+
}
133+
}
134+
}
135+
}
136+
}""");
137+
assertOK(adminClient().performRequest(mappingsRequest));
138+
139+
String settingsTemplateName = name + "_settings";
140+
Request settingsRequest = new Request("PUT", "/_component_template/" + settingsTemplateName);
141+
settingsRequest.setJsonEntity("""
142+
{
143+
"template": {
144+
"settings": {
145+
"number_of_shards": 1,
146+
"number_of_replicas": 0
147+
}
148+
}
149+
}""");
150+
assertOK(adminClient().performRequest(settingsRequest));
151+
152+
Request indexTemplateRequest = new Request("PUT", "/_index_template/" + name + "_template");
153+
indexTemplateRequest.setJsonEntity(Strings.format("""
154+
{
155+
"index_patterns": ["%s*"],
156+
"data_stream": { },
157+
"composed_of": [ "%s", "%s" ]
158+
}""", name, mappingsTemplateName, settingsTemplateName));
159+
assertOK(adminClient().performRequest(indexTemplateRequest));
160+
161+
Request request = new Request("PUT", "/_data_stream/" + name);
162+
assertOK(adminClient().performRequest(request));
163+
}
164+
165+
}

x-pack/docs/en/security/authorization/privileges.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,12 @@ All {Ilm} operations relating to managing the execution of policies of an index
303303
or data stream. This includes operations such as retrying policies and removing
304304
a policy from an index or data stream.
305305

306+
ifeval::["{release-state}"!="released"]
307+
`manage_dlm`::
308+
All {Dlm} operations relating to reading and managing the lifecycle of a data stream.
309+
This includes operations such as adding and removing a lifecycle from a data stream.
310+
endif::[]
311+
306312
`manage_leader_index`::
307313
All actions that are required to manage the lifecycle of a leader index, which
308314
includes <<ccr-post-forget-follower,forgetting a follower>>. This

0 commit comments

Comments
 (0)