Skip to content

Commit cb83936

Browse files
[8.x] [Failure Store] Manage-style privileges grant both data and failures access (#125900) (#126044)
* [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. * Fix imports --------- Co-authored-by: Slobodan Adamović <[email protected]>
1 parent 6cd585b commit cb83936

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

+4
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

+23-6
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

+70-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.common.util.iterable.Iterables;
1919
import org.elasticsearch.test.ESTestCase;
2020
import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction;
21+
import org.elasticsearch.xpack.core.security.support.Automatons;
2122
import org.elasticsearch.xpack.core.transform.action.GetCheckpointAction;
2223

2324
import java.util.Collection;
@@ -240,6 +241,70 @@ public void testResolveBySelectorAccess() {
240241
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
241242
assertThat(actualPredicates, containsInAnyOrder(IndexComponentSelectorPredicate.ALL));
242243
}
244+
{
245+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
246+
Set.of("manage", "all", "read", "indices:data/read/search", "view_index_metadata")
247+
);
248+
assertThat(
249+
actual,
250+
containsInAnyOrder(
251+
resolvePrivilegeAndAssertSingleton(Set.of("manage", "all", "read", "indices:data/read/search", "view_index_metadata"))
252+
)
253+
);
254+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
255+
assertThat(actualPredicates, containsInAnyOrder(IndexComponentSelectorPredicate.ALL));
256+
}
257+
{
258+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
259+
Set.of("manage", "read", "indices:data/read/search", "read_failure_store")
260+
);
261+
assertThat(
262+
actual,
263+
containsInAnyOrder(
264+
IndexPrivilege.MANAGE,
265+
IndexPrivilege.READ_FAILURE_STORE,
266+
resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:data/read/search"))
267+
)
268+
);
269+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
270+
assertThat(
271+
actualPredicates,
272+
containsInAnyOrder(
273+
IndexComponentSelectorPredicate.DATA,
274+
IndexComponentSelectorPredicate.FAILURES,
275+
IndexComponentSelectorPredicate.DATA_AND_FAILURES
276+
)
277+
);
278+
}
279+
{
280+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(Set.of("manage", "read", "indices:data/read/search"));
281+
assertThat(
282+
actual,
283+
containsInAnyOrder(IndexPrivilege.MANAGE, resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:data/read/search")))
284+
);
285+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
286+
assertThat(
287+
actualPredicates,
288+
containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.DATA_AND_FAILURES)
289+
);
290+
}
291+
{
292+
Set<IndexPrivilege> actual = IndexPrivilege.resolveBySelectorAccess(
293+
Set.of("manage", "read", "manage_data_stream_lifecycle", "indices:admin/*")
294+
);
295+
assertThat(
296+
actual,
297+
containsInAnyOrder(
298+
resolvePrivilegeAndAssertSingleton(Set.of("manage_data_stream_lifecycle", "manage")),
299+
resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:admin/*"))
300+
)
301+
);
302+
List<IndexComponentSelectorPredicate> actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList();
303+
assertThat(
304+
actualPredicates,
305+
containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.DATA_AND_FAILURES)
306+
);
307+
}
243308
}
244309

245310
public void testPrivilegesForRollupFieldCapsAction() {
@@ -300,7 +365,11 @@ public void testCrossClusterReplicationPrivileges() {
300365
assertThat(
301366
Operations.subsetOf(
302367
crossClusterReplication.automaton,
303-
resolvePrivilegeAndAssertSingleton(Set.of("manage", "read", "monitor")).automaton
368+
IndexPrivilege.resolveBySelectorAccess(Set.of("manage", "read", "monitor"))
369+
.stream()
370+
.map(p -> p.automaton)
371+
.reduce((a1, a2) -> Automatons.unionAndMinimize(List.of(a1, a2)))
372+
.get()
304373
),
305374
is(true)
306375
);

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

+89-16
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,12 +1848,45 @@ 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);

0 commit comments

Comments
 (0)