Skip to content

Commit 0e0214d

Browse files
authored
[Failure Store] Has Privileges API (#125329)
This PR adds support for checking access to the failure store via the Has Privileges API. To check access for a data stream `logs`, a request must query for a concrete named privilege, `read_failure_store` or `manage_failure_store`, e.g., a request to the HasPrivileges API by a user with `read_failure_store` over `logs`: ``` POST /_security/user/_has_privileges { "index": [ { "names": ["logs"], "privileges": ["read_failure_store", "read", "indices:data/read/*"] } ] } ``` Returns: ``` { "username": "<...>", "has_all_requested": false, "cluster": {}, "index": { "logs": { "read_failure_store": true, "read": false, <1> "indices:data/read/*": false <2> } }, "application": {} } ``` Note that `<1>` and `<2>` are both `false` since `read` is not covered by `read_failure_store` and neither are any raw actions like `indices:data/read/*` since these implicitly correspond to data access. Selectors are not allowed in the index patterns of HasPrivileges requests to avoid ambiguities such as checking `read` on `logs::failures` as well as the ambiguity of index patterns that are regular expressions.
1 parent 3d9a7ab commit 0e0214d

File tree

4 files changed

+810
-51
lines changed

4 files changed

+810
-51
lines changed

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

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,27 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio
340340
if (index == null) {
341341
validationException = addValidationError("indexPrivileges must not be null", validationException);
342342
} else {
343-
for (int i = 0; i < index.length; i++) {
344-
BytesReference query = index[i].getQuery();
343+
for (RoleDescriptor.IndicesPrivileges indicesPrivileges : index) {
344+
BytesReference query = indicesPrivileges.getQuery();
345345
if (query != null) {
346346
validationException = addValidationError(
347347
"may only check index privileges without any DLS query [" + query.utf8ToString() + "]",
348348
validationException
349349
);
350350
}
351+
if (DataStream.isFailureStoreFeatureFlagEnabled()) {
352+
// best effort prevent users from attempting to use selectors in privilege check
353+
for (String indexPattern : indicesPrivileges.getIndices()) {
354+
if (IndexNameExpressionResolver.hasSelector(indexPattern, IndexComponentSelector.FAILURES)
355+
|| IndexNameExpressionResolver.hasSelector(indexPattern, IndexComponentSelector.DATA)) {
356+
validationException = addValidationError(
357+
"may only check index privileges without selectors in index patterns [" + indexPattern + "]",
358+
validationException
359+
);
360+
break;
361+
}
362+
}
363+
}
351364
}
352365
}
353366
if (application == null) {
@@ -369,31 +382,6 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio
369382
&& application.length == 0) {
370383
validationException = addValidationError("must specify at least one privilege", validationException);
371384
}
372-
if (index != null) {
373-
// no need to validate failure-store related constraints if it's not enabled
374-
if (DataStream.isFailureStoreFeatureFlagEnabled()) {
375-
for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) {
376-
if (indexPrivilege.getIndices() != null
377-
&& Arrays.stream(indexPrivilege.getIndices())
378-
// best effort prevent users from attempting to check failure selectors
379-
.anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) {
380-
validationException = addValidationError(
381-
// TODO adjust message once HasPrivileges check supports checking failure store privileges
382-
"failures selector is not supported in index patterns",
383-
validationException
384-
);
385-
}
386-
if (indexPrivilege.getPrivileges() != null
387-
&& Arrays.stream(indexPrivilege.getPrivileges())
388-
.anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) {
389-
validationException = addValidationError(
390-
"checking failure store privileges is not supported",
391-
validationException
392-
);
393-
}
394-
}
395-
}
396-
}
397385
return validationException;
398386
}
399387

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -317,36 +317,58 @@ public boolean checkResourcePrivileges(
317317
@Nullable ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder
318318
) {
319319
boolean allMatch = true;
320-
Map<Automaton, Automaton> indexGroupAutomatons = indexGroupAutomatons(
321-
combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex)
320+
Map<Automaton, Automaton> indexGroupAutomatonsForDataSelector = indexGroupAutomatons(
321+
combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex),
322+
IndexComponentSelector.DATA
322323
);
324+
// optimization: if there are no failures selector privileges in the set of privileges to check, we can skip building
325+
// the automaton map
326+
final boolean containsPrivilegesForFailuresSelector = containsPrivilegesForFailuresSelector(checkForPrivileges);
327+
Map<Automaton, Automaton> indexGroupAutomatonsForFailuresSelector = false == containsPrivilegesForFailuresSelector
328+
? Map.of()
329+
: indexGroupAutomatons(
330+
combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex),
331+
IndexComponentSelector.FAILURES
332+
);
323333
for (String forIndexPattern : checkForIndexPatterns) {
324-
IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(forIndexPattern);
325334
Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern);
326335
if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) {
327336
checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, restrictedIndices.getAutomaton());
328337
}
329338
if (false == Operations.isEmpty(checkIndexAutomaton)) {
330-
Automaton allowedIndexPrivilegesAutomaton = null;
331-
for (var indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) {
332-
if (Automatons.subsetOf(checkIndexAutomaton, indexAndPrivilegeAutomaton.getValue())) {
333-
if (allowedIndexPrivilegesAutomaton != null) {
334-
allowedIndexPrivilegesAutomaton = Automatons.unionAndMinimize(
335-
Arrays.asList(allowedIndexPrivilegesAutomaton, indexAndPrivilegeAutomaton.getKey())
336-
);
337-
} else {
338-
allowedIndexPrivilegesAutomaton = indexAndPrivilegeAutomaton.getKey();
339-
}
340-
}
341-
}
339+
Automaton allowedPrivilegesAutomatonForDataSelector = getIndexPrivilegesAutomaton(
340+
indexGroupAutomatonsForDataSelector,
341+
checkIndexAutomaton
342+
);
343+
Automaton allowedPrivilegesAutomatonForFailuresSelector = getIndexPrivilegesAutomaton(
344+
indexGroupAutomatonsForFailuresSelector,
345+
checkIndexAutomaton
346+
);
342347
for (String privilege : checkForPrivileges) {
343-
IndexPrivilege indexPrivilege = IndexPrivilege.get(privilege);
344-
if (allowedIndexPrivilegesAutomaton != null
345-
&& Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
348+
final IndexPrivilege indexPrivilege = IndexPrivilege.get(privilege);
349+
final boolean checkWithDataSelector = indexPrivilege.getSelectorPredicate().test(IndexComponentSelector.DATA);
350+
final boolean checkWithFailuresSelector = indexPrivilege.getSelectorPredicate().test(IndexComponentSelector.FAILURES);
351+
assert checkWithDataSelector || checkWithFailuresSelector
352+
: "index privilege must map to at least one of [data, failures] selectors";
353+
assert containsPrivilegesForFailuresSelector
354+
|| indexPrivilege.getSelectorPredicate() != IndexComponentSelectorPredicate.FAILURES
355+
: "no failures access privileges should be present in the set of privileges to check";
356+
final Automaton automatonToCheck = indexPrivilege.getAutomaton();
357+
if (checkWithDataSelector
358+
&& allowedPrivilegesAutomatonForDataSelector != null
359+
&& Automatons.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForDataSelector)) {
346360
if (resourcePrivilegesMapBuilder != null) {
347361
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
348362
}
349-
} else {
363+
} else if (checkWithFailuresSelector
364+
&& allowedPrivilegesAutomatonForFailuresSelector != null
365+
&& Automatons.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForFailuresSelector)) {
366+
if (resourcePrivilegesMapBuilder != null) {
367+
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
368+
}
369+
}
370+
// comment to force correct else-block indent
371+
else {
350372
if (resourcePrivilegesMapBuilder != null) {
351373
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
352374
allMatch = false;
@@ -806,13 +828,11 @@ private static boolean containsPrivilegeThatGrantsMappingUpdatesForBwc(Group gro
806828
*
807829
* @return a map of all index and privilege pattern automatons
808830
*/
809-
private Map<Automaton, Automaton> indexGroupAutomatons(boolean combine) {
831+
private Map<Automaton, Automaton> indexGroupAutomatons(boolean combine, IndexComponentSelector selector) {
810832
// Map of privilege automaton object references (cached by IndexPrivilege::CACHE)
811833
Map<Automaton, Automaton> allAutomatons = new HashMap<>();
812834
for (Group group : groups) {
813-
// TODO support failure store privileges
814-
// we also check that the group does not support data access to avoid erroneously filtering out `all` privilege groups
815-
if (group.checkSelector(IndexComponentSelector.FAILURES) && false == group.checkSelector(IndexComponentSelector.DATA)) {
835+
if (false == group.checkSelector(selector)) {
816836
continue;
817837
}
818838
Automaton indexAutomaton = group.getIndexMatcherAutomaton();
@@ -845,6 +865,41 @@ private Map<Automaton, Automaton> indexGroupAutomatons(boolean combine) {
845865
return allAutomatons;
846866
}
847867

868+
private static boolean containsPrivilegesForFailuresSelector(Set<String> checkForPrivileges) {
869+
for (String privilege : checkForPrivileges) {
870+
// use `getNamedOrNull` since only a named privilege can be a failures-only privilege (raw action names are always data access)
871+
IndexPrivilege named = IndexPrivilege.getNamedOrNull(privilege);
872+
// note: we are looking for failures-only privileges here, not `all` which does cover failures but is not a failures-only
873+
// privilege
874+
if (named != null && named.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) {
875+
return true;
876+
}
877+
}
878+
return false;
879+
}
880+
881+
@Nullable
882+
private static Automaton getIndexPrivilegesAutomaton(Map<Automaton, Automaton> indexGroupAutomatons, Automaton checkIndexAutomaton) {
883+
if (indexGroupAutomatons.isEmpty()) {
884+
return null;
885+
}
886+
Automaton allowedPrivilegesAutomaton = null;
887+
for (Map.Entry<Automaton, Automaton> indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) {
888+
Automaton indexNameAutomaton = indexAndPrivilegeAutomaton.getValue();
889+
if (Automatons.subsetOf(checkIndexAutomaton, indexNameAutomaton)) {
890+
Automaton privilegesAutomaton = indexAndPrivilegeAutomaton.getKey();
891+
if (allowedPrivilegesAutomaton != null) {
892+
allowedPrivilegesAutomaton = Automatons.unionAndMinimize(
893+
Arrays.asList(allowedPrivilegesAutomaton, privilegesAutomaton)
894+
);
895+
} else {
896+
allowedPrivilegesAutomaton = privilegesAutomaton;
897+
}
898+
}
899+
}
900+
return allowedPrivilegesAutomaton;
901+
}
902+
848903
public static class Group {
849904
public static final Group[] EMPTY_ARRAY = new Group[0];
850905

0 commit comments

Comments
 (0)