Skip to content

Commit 3be4fb1

Browse files
authored
[Failure Store] Manage-style privileges grant both data and failures access (#125900)
It's more natural for `manage` and `manage_data_stream_lifecycle` to grant access to management style APIs both for regular data streams and their failure stores. This PR adds support for privileges to grant access to both data and failures selectors (without granting access to everything, à la `all`), and extends `manage` and `manage_data_stream_lifecycle` to grant failure store access, in addition to regular data stream access. `manage_failure_store` still grants failures-only access.
1 parent a97e006 commit 3be4fb1

File tree

7 files changed

+339
-28
lines changed

7 files changed

+339
-28
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public record IndexComponentSelectorPredicate(Set<String> names, Predicate<Index
3434
"failures",
3535
IndexComponentSelector.FAILURES::equals
3636
);
37+
public static final IndexComponentSelectorPredicate DATA_AND_FAILURES = new IndexComponentSelectorPredicate(
38+
Set.of("data", "failures"),
39+
DATA.predicate.or(FAILURES.predicate)
40+
);
3741

3842
@Override
3943
public boolean test(IndexComponentSelector selector) {

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
import java.util.Collections;
5151
import java.util.HashSet;
5252
import java.util.LinkedHashMap;
53-
import java.util.LinkedHashSet;
5453
import java.util.Locale;
5554
import java.util.Map;
5655
import java.util.Objects;
@@ -195,7 +194,11 @@ public final class IndexPrivilege extends Privilege {
195194
public static final IndexPrivilege WRITE = new IndexPrivilege("write", WRITE_AUTOMATON);
196195
public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON);
197196
public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON);
198-
public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON);
197+
public static final IndexPrivilege MANAGE = new IndexPrivilege(
198+
"manage",
199+
MANAGE_AUTOMATON,
200+
IndexComponentSelectorPredicate.DATA_AND_FAILURES
201+
);
199202
public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON);
200203
public static final IndexPrivilege CREATE_INDEX = new IndexPrivilege("create_index", CREATE_INDEX_AUTOMATON);
201204
public static final IndexPrivilege VIEW_METADATA = new IndexPrivilege("view_index_metadata", VIEW_METADATA_AUTOMATON);
@@ -204,7 +207,8 @@ public final class IndexPrivilege extends Privilege {
204207
public static final IndexPrivilege MANAGE_ILM = new IndexPrivilege("manage_ilm", MANAGE_ILM_AUTOMATON);
205208
public static final IndexPrivilege MANAGE_DATA_STREAM_LIFECYCLE = new IndexPrivilege(
206209
"manage_data_stream_lifecycle",
207-
MANAGE_DATA_STREAM_LIFECYCLE_AUTOMATON
210+
MANAGE_DATA_STREAM_LIFECYCLE_AUTOMATON,
211+
IndexComponentSelectorPredicate.DATA_AND_FAILURES
208212
);
209213
public static final IndexPrivilege MAINTENANCE = new IndexPrivilege("maintenance", MAINTENANCE_AUTOMATON);
210214
public static final IndexPrivilege AUTO_CONFIGURE = new IndexPrivilege("auto_configure", AUTO_CONFIGURE_AUTOMATON);
@@ -364,6 +368,7 @@ private static Set<IndexPrivilege> resolve(Set<String> name) {
364368
final Set<IndexPrivilege> allSelectorAccessPrivileges = new HashSet<>();
365369
final Set<IndexPrivilege> dataSelectorAccessPrivileges = new HashSet<>();
366370
final Set<IndexPrivilege> failuresSelectorAccessPrivileges = new HashSet<>();
371+
final Set<IndexPrivilege> dataAndFailuresSelectorAccessPrivileges = new HashSet<>();
367372

368373
boolean containsAllAccessPrivilege = name.stream().anyMatch(n -> getNamedOrNull(n) == ALL);
369374
for (String part : name) {
@@ -383,6 +388,8 @@ private static Set<IndexPrivilege> resolve(Set<String> name) {
383388
dataSelectorAccessPrivileges.add(indexPrivilege);
384389
} else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.FAILURES) {
385390
failuresSelectorAccessPrivileges.add(indexPrivilege);
391+
} else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.DATA_AND_FAILURES) {
392+
dataAndFailuresSelectorAccessPrivileges.add(indexPrivilege);
386393
} else {
387394
String errorMessage = "unexpected selector [" + indexPrivilege.selectorPredicate + "]";
388395
assert false : errorMessage;
@@ -406,6 +413,7 @@ private static Set<IndexPrivilege> resolve(Set<String> name) {
406413
allSelectorAccessPrivileges,
407414
dataSelectorAccessPrivileges,
408415
failuresSelectorAccessPrivileges,
416+
dataAndFailuresSelectorAccessPrivileges,
409417
actions
410418
);
411419
assertNamesMatch(name, combined);
@@ -416,24 +424,33 @@ private static Set<IndexPrivilege> combineIndexPrivileges(
416424
Set<IndexPrivilege> allSelectorAccessPrivileges,
417425
Set<IndexPrivilege> dataSelectorAccessPrivileges,
418426
Set<IndexPrivilege> failuresSelectorAccessPrivileges,
427+
Set<IndexPrivilege> dataAndFailuresSelectorAccessPrivileges,
419428
Set<String> actions
420429
) {
421430
assert false == allSelectorAccessPrivileges.isEmpty()
422431
|| false == dataSelectorAccessPrivileges.isEmpty()
423432
|| false == failuresSelectorAccessPrivileges.isEmpty()
433+
|| false == dataAndFailuresSelectorAccessPrivileges.isEmpty()
424434
|| false == actions.isEmpty() : "at least one of the privilege sets or actions must be non-empty";
425435

426436
if (false == allSelectorAccessPrivileges.isEmpty()) {
427-
assert failuresSelectorAccessPrivileges.isEmpty() && dataSelectorAccessPrivileges.isEmpty()
428-
: "data and failure access must be empty when all access is present";
437+
assert failuresSelectorAccessPrivileges.isEmpty()
438+
&& dataSelectorAccessPrivileges.isEmpty()
439+
&& dataAndFailuresSelectorAccessPrivileges.isEmpty() : "data and failure access must be empty when all access is present";
429440
return Set.of(union(allSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.ALL));
430441
}
431442

432443
// linked hash set to preserve order across selectors
433-
final Set<IndexPrivilege> combined = new LinkedHashSet<>();
444+
final Set<IndexPrivilege> combined = Sets.newLinkedHashSetWithExpectedSize(
445+
dataAndFailuresSelectorAccessPrivileges.size() + failuresSelectorAccessPrivileges.size() + dataSelectorAccessPrivileges.size()
446+
+ actions.size()
447+
);
434448
if (false == dataSelectorAccessPrivileges.isEmpty() || false == actions.isEmpty()) {
435449
combined.add(union(dataSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.DATA));
436450
}
451+
if (false == dataAndFailuresSelectorAccessPrivileges.isEmpty()) {
452+
combined.add(union(dataAndFailuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.DATA_AND_FAILURES));
453+
}
437454
if (false == failuresSelectorAccessPrivileges.isEmpty()) {
438455
combined.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES));
439456
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,70 @@ public void testResolveBySelectorAccess() {
240240
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
241241
assertThat(actualPredicates, containsInAnyOrder(IndexComponentSelectorPredicate.ALL));
242242
}
243+
{
244+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
245+
Set.of("manage", "all", "read", "indices:data/read/search", "view_index_metadata")
246+
);
247+
assertThat(
248+
actual,
249+
containsInAnyOrder(
250+
resolvePrivilegeAndAssertSingleton(Set.of("manage", "all", "read", "indices:data/read/search", "view_index_metadata"))
251+
)
252+
);
253+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
254+
assertThat(actualPredicates, containsInAnyOrder(IndexComponentSelectorPredicate.ALL));
255+
}
256+
{
257+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
258+
Set.of("manage", "read", "indices:data/read/search", "read_failure_store")
259+
);
260+
assertThat(
261+
actual,
262+
containsInAnyOrder(
263+
IndexPrivilege.MANAGE,
264+
IndexPrivilege.READ_FAILURE_STORE,
265+
resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:data/read/search"))
266+
)
267+
);
268+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
269+
assertThat(
270+
actualPredicates,
271+
containsInAnyOrder(
272+
IndexComponentSelectorPredicate.DATA,
273+
IndexComponentSelectorPredicate.FAILURES,
274+
IndexComponentSelectorPredicate.DATA_AND_FAILURES
275+
)
276+
);
277+
}
278+
{
279+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(Set.of("manage", "read", "indices:data/read/search"));
280+
assertThat(
281+
actual,
282+
containsInAnyOrder(IndexPrivilege.MANAGE, resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:data/read/search")))
283+
);
284+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
285+
assertThat(
286+
actualPredicates,
287+
containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.DATA_AND_FAILURES)
288+
);
289+
}
290+
{
291+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
292+
Set.of("manage", "read", "manage_data_stream_lifecycle", "indices:admin/*")
293+
);
294+
assertThat(
295+
actual,
296+
containsInAnyOrder(
297+
resolvePrivilegeAndAssertSingleton(Set.of("manage_data_stream_lifecycle", "manage")),
298+
resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:admin/*"))
299+
)
300+
);
301+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
302+
assertThat(
303+
actualPredicates,
304+
containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.DATA_AND_FAILURES)
305+
);
306+
}
243307
}
244308

245309
public void testPrivilegesForRollupFieldCapsAction() {
@@ -300,7 +364,11 @@ public void testCrossClusterReplicationPrivileges() {
300364
assertThat(
301365
Automatons.subsetOf(
302366
crossClusterReplication.automaton,
303-
resolvePrivilegeAndAssertSingleton(Set.of("manage", "read", "monitor")).automaton
367+
IndexPrivilege.resolveBySelectorAccess(Set.of("manage", "read", "monitor"))
368+
.stream()
369+
.map(p -> p.automaton)
370+
.reduce((a1, a2) -> Automatons.unionAndMinimize(List.of(a1, a2)))
371+
.get()
304372
),
305373
is(true)
306374
);

x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ protected Settings restAdminSettings() {
9393
private static final String WRITE_ACCESS = "write_access";
9494
private static final String MANAGE_ACCESS = "manage_access";
9595
private static final String MANAGE_FAILURE_STORE_ACCESS = "manage_failure_store_access";
96+
private static final String MANAGE_DATA_STREAM_LIFECYCLE = "manage_data_stream_lifecycle";
9697
private static final SecureString PASSWORD = new SecureString("admin-password");
9798

9899
@Before
@@ -126,14 +127,10 @@ public void testGetUserPrivileges() throws IOException {
126127
"global": [],
127128
"indices": [{
128129
"names": ["*"],
129-
"privileges": ["read"],
130+
"privileges": ["read", "read_failure_store"],
130131
"allow_restricted_indices": false
131-
},
132-
{
133-
"names": ["*"],
134-
"privileges": ["read_failure_store"],
135-
"allow_restricted_indices": false
136-
}],
132+
}
133+
],
137134
"applications": [],
138135
"run_as": []
139136
}""");
@@ -210,14 +207,57 @@ public void testGetUserPrivileges() throws IOException {
210207
"indices": [
211208
{
212209
"names": ["*"],
213-
"privileges": ["read", "write"],
210+
"privileges": ["manage_failure_store", "read", "read_failure_store", "write"],
211+
"allow_restricted_indices": false
212+
}
213+
],
214+
"applications": [],
215+
"run_as": []
216+
}""");
217+
218+
upsertRole("""
219+
{
220+
"cluster": ["all"],
221+
"indices": [
222+
{
223+
"names": ["*", "idx"],
224+
"privileges": ["read", "manage"],
214225
"allow_restricted_indices": false
215226
},
216227
{
217-
"names": ["*"],
218-
"privileges": ["manage_failure_store", "read_failure_store"],
228+
"names": ["idx", "*"],
229+
"privileges": ["manage_data_stream_lifecycle"],
219230
"allow_restricted_indices": false
220-
}],
231+
},
232+
{
233+
"names": ["*", "idx"],
234+
"privileges": ["write"],
235+
"allow_restricted_indices": true
236+
},
237+
{
238+
"names": ["idx", "*"],
239+
"privileges": ["manage"],
240+
"allow_restricted_indices": true
241+
}
242+
]
243+
}
244+
""", "role");
245+
expectUserPrivilegesResponse("""
246+
{
247+
"cluster": ["all"],
248+
"global": [],
249+
"indices": [
250+
{
251+
"names": ["*", "idx"],
252+
"privileges": ["manage", "manage_data_stream_lifecycle", "read"],
253+
"allow_restricted_indices": false
254+
},
255+
{
256+
"names": ["*", "idx"],
257+
"privileges": ["manage", "write"],
258+
"allow_restricted_indices": true
259+
}
260+
],
221261
"applications": [],
222262
"run_as": []
223263
}""");
@@ -1772,7 +1812,7 @@ public void testFailureStoreAccess() throws Exception {
17721812
}
17731813
}
17741814

1775-
public void testWriteOperations() throws IOException {
1815+
public void testWriteAndManageOperations() throws IOException {
17761816
setupDataStream();
17771817
Tuple<String, String> backingIndices = getSingleDataAndFailureIndices("test1");
17781818
String dataIndexName = backingIndices.v1();
@@ -1808,15 +1848,49 @@ public void testWriteOperations() throws IOException {
18081848
}
18091849
""");
18101850

1811-
// user with manage access to data stream does NOT get direct access to failure index
1812-
expectThrows(() -> deleteIndex(MANAGE_ACCESS, failureIndexName), 403);
1851+
createUser(MANAGE_DATA_STREAM_LIFECYCLE, PASSWORD, MANAGE_DATA_STREAM_LIFECYCLE);
1852+
upsertRole(Strings.format("""
1853+
{
1854+
"cluster": ["all"],
1855+
"indices": [{"names": ["test*"], "privileges": ["manage_data_stream_lifecycle"]}]
1856+
}"""), MANAGE_DATA_STREAM_LIFECYCLE);
1857+
createAndStoreApiKey(MANAGE_DATA_STREAM_LIFECYCLE, randomBoolean() ? null : """
1858+
{
1859+
"role": {
1860+
"cluster": ["all"],
1861+
"indices": [{"names": ["test*"], "privileges": ["manage_data_stream_lifecycle"]}]
1862+
}
1863+
}
1864+
""");
1865+
1866+
// explain lifecycle API with and without failures selector is granted by manage
1867+
assertOK(performRequest(MANAGE_ACCESS, new Request("GET", "test1/_lifecycle/explain")));
1868+
assertOK(performRequest(MANAGE_ACCESS, new Request("GET", "test1::failures/_lifecycle/explain")));
1869+
assertOK(performRequest(MANAGE_ACCESS, new Request("GET", failureIndexName + "/_lifecycle/explain")));
1870+
assertOK(performRequest(MANAGE_ACCESS, new Request("GET", dataIndexName + "/_lifecycle/explain")));
1871+
1872+
assertOK(performRequest(MANAGE_DATA_STREAM_LIFECYCLE, new Request("GET", "test1/_lifecycle/explain")));
1873+
assertOK(performRequest(MANAGE_DATA_STREAM_LIFECYCLE, new Request("GET", "test1::failures/_lifecycle/explain")));
1874+
assertOK(performRequest(MANAGE_DATA_STREAM_LIFECYCLE, new Request("GET", failureIndexName + "/_lifecycle/explain")));
1875+
assertOK(performRequest(MANAGE_DATA_STREAM_LIFECYCLE, new Request("GET", dataIndexName + "/_lifecycle/explain")));
1876+
1877+
// explain lifecycle API is granted by manage_failure_store only for failures selector
1878+
expectThrows(() -> performRequest(MANAGE_FAILURE_STORE_ACCESS, new Request("GET", "test1/_lifecycle/explain")), 403);
1879+
assertOK(performRequest(MANAGE_FAILURE_STORE_ACCESS, new Request("GET", "test1::failures/_lifecycle/explain")));
1880+
assertOK(performRequest(MANAGE_FAILURE_STORE_ACCESS, new Request("GET", failureIndexName + "/_lifecycle/explain")));
1881+
expectThrows(() -> performRequest(MANAGE_FAILURE_STORE_ACCESS, new Request("GET", dataIndexName + "/_lifecycle/explain")), 403);
1882+
1883+
// user with manage access to data stream can delete failure index because manage grants access to both data and failures
1884+
expectThrows(() -> deleteIndex(MANAGE_ACCESS, failureIndexName), 400);
18131885
expectThrows(() -> deleteIndex(MANAGE_ACCESS, dataIndexName), 400);
1814-
// manage_failure_store user COULD delete failure index (not valid because it's a write index, but allow security-wise)
1815-
expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403);
1886+
1887+
// manage_failure_store user COULD delete failure index (not valid because it's a write index, but allowed security-wise)
18161888
expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, failureIndexName), 400);
1889+
expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403);
18171890
expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403);
18181891

18191892
expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1"), 403);
1893+
// selectors aren't supported for deletes so we get a 403
18201894
expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1::failures"), 403);
18211895

18221896
// manage user can delete data stream

0 commit comments

Comments
 (0)