Browse Source

[8.x] [Failure Store] Has Privileges API (#125329) (#125932)

* [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.

(cherry picked from commit 0e0214dcc2658ba81a2c00b75ae42e16a111f6e3)

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

* fix compilation issue: use Operations.subsetOf

* fix one more place

---------

Co-authored-by: Nikolaj Volgushev <n1v0lg@users.noreply.github.com>
Slobodan Adamović 6 months ago
parent
commit
4cbee5de02

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

@@ -339,14 +339,27 @@ public interface AuthorizationEngine {
             if (index == null) {
                 validationException = addValidationError("indexPrivileges must not be null", validationException);
             } else {
-                for (int i = 0; i < index.length; i++) {
-                    BytesReference query = index[i].getQuery();
+                for (RoleDescriptor.IndicesPrivileges indicesPrivileges : index) {
+                    BytesReference query = indicesPrivileges.getQuery();
                     if (query != null) {
                         validationException = addValidationError(
                             "may only check index privileges without any DLS query [" + query.utf8ToString() + "]",
                             validationException
                         );
                     }
+                    if (DataStream.isFailureStoreFeatureFlagEnabled()) {
+                        // best effort prevent users from attempting to use selectors in privilege check
+                        for (String indexPattern : indicesPrivileges.getIndices()) {
+                            if (IndexNameExpressionResolver.hasSelector(indexPattern, IndexComponentSelector.FAILURES)
+                                || IndexNameExpressionResolver.hasSelector(indexPattern, IndexComponentSelector.DATA)) {
+                                validationException = addValidationError(
+                                    "may only check index privileges without selectors in index patterns [" + indexPattern + "]",
+                                    validationException
+                                );
+                                break;
+                            }
+                        }
+                    }
                 }
             }
             if (application == null) {
@@ -368,31 +381,6 @@ public interface AuthorizationEngine {
                 && application.length == 0) {
                 validationException = addValidationError("must specify at least one privilege", validationException);
             }
-            if (index != null) {
-                // no need to validate failure-store related constraints if it's not enabled
-                if (DataStream.isFailureStoreFeatureFlagEnabled()) {
-                    for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) {
-                        if (indexPrivilege.getIndices() != null
-                            && Arrays.stream(indexPrivilege.getIndices())
-                                // best effort prevent users from attempting to check failure selectors
-                                .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) {
-                            validationException = addValidationError(
-                                // TODO adjust message once HasPrivileges check supports checking failure store privileges
-                                "failures selector is not supported in index patterns",
-                                validationException
-                            );
-                        }
-                        if (indexPrivilege.getPrivileges() != null
-                            && Arrays.stream(indexPrivilege.getPrivileges())
-                                .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) {
-                            validationException = addValidationError(
-                                "checking failure store privileges is not supported",
-                                validationException
-                            );
-                        }
-                    }
-                }
-            }
             return validationException;
         }
 

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

@@ -317,36 +317,58 @@ public final class IndicesPermission {
         @Nullable ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder
     ) {
         boolean allMatch = true;
-        Map<Automaton, Automaton> indexGroupAutomatons = indexGroupAutomatons(
-            combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex)
+        Map<Automaton, Automaton> indexGroupAutomatonsForDataSelector = indexGroupAutomatons(
+            combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex),
+            IndexComponentSelector.DATA
         );
+        // optimization: if there are no failures selector privileges in the set of privileges to check, we can skip building
+        // the automaton map
+        final boolean containsPrivilegesForFailuresSelector = containsPrivilegesForFailuresSelector(checkForPrivileges);
+        Map<Automaton, Automaton> indexGroupAutomatonsForFailuresSelector = false == containsPrivilegesForFailuresSelector
+            ? Map.of()
+            : indexGroupAutomatons(
+                combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex),
+                IndexComponentSelector.FAILURES
+            );
         for (String forIndexPattern : checkForIndexPatterns) {
-            IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(forIndexPattern);
             Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern);
             if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) {
                 checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, restrictedIndices.getAutomaton());
             }
             if (false == Operations.isEmpty(checkIndexAutomaton)) {
-                Automaton allowedIndexPrivilegesAutomaton = null;
-                for (var indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) {
-                    if (Operations.subsetOf(checkIndexAutomaton, indexAndPrivilegeAutomaton.getValue())) {
-                        if (allowedIndexPrivilegesAutomaton != null) {
-                            allowedIndexPrivilegesAutomaton = Automatons.unionAndMinimize(
-                                Arrays.asList(allowedIndexPrivilegesAutomaton, indexAndPrivilegeAutomaton.getKey())
-                            );
-                        } else {
-                            allowedIndexPrivilegesAutomaton = indexAndPrivilegeAutomaton.getKey();
-                        }
-                    }
-                }
+                Automaton allowedPrivilegesAutomatonForDataSelector = getIndexPrivilegesAutomaton(
+                    indexGroupAutomatonsForDataSelector,
+                    checkIndexAutomaton
+                );
+                Automaton allowedPrivilegesAutomatonForFailuresSelector = getIndexPrivilegesAutomaton(
+                    indexGroupAutomatonsForFailuresSelector,
+                    checkIndexAutomaton
+                );
                 for (String privilege : checkForPrivileges) {
-                    IndexPrivilege indexPrivilege = IndexPrivilege.get(privilege);
-                    if (allowedIndexPrivilegesAutomaton != null
-                        && Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
+                    final IndexPrivilege indexPrivilege = IndexPrivilege.get(privilege);
+                    final boolean checkWithDataSelector = indexPrivilege.getSelectorPredicate().test(IndexComponentSelector.DATA);
+                    final boolean checkWithFailuresSelector = indexPrivilege.getSelectorPredicate().test(IndexComponentSelector.FAILURES);
+                    assert checkWithDataSelector || checkWithFailuresSelector
+                        : "index privilege must map to at least one of [data, failures] selectors";
+                    assert containsPrivilegesForFailuresSelector
+                        || indexPrivilege.getSelectorPredicate() != IndexComponentSelectorPredicate.FAILURES
+                        : "no failures access privileges should be present in the set of privileges to check";
+                    final Automaton automatonToCheck = indexPrivilege.getAutomaton();
+                    if (checkWithDataSelector
+                        && allowedPrivilegesAutomatonForDataSelector != null
+                        && Operations.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForDataSelector)) {
                         if (resourcePrivilegesMapBuilder != null) {
                             resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
                         }
-                    } else {
+                    } else if (checkWithFailuresSelector
+                        && allowedPrivilegesAutomatonForFailuresSelector != null
+                        && Operations.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForFailuresSelector)) {
+                            if (resourcePrivilegesMapBuilder != null) {
+                                resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
+                            }
+                        }
+                    // comment to force correct else-block indent
+                    else {
                         if (resourcePrivilegesMapBuilder != null) {
                             resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
                             allMatch = false;
@@ -806,13 +828,11 @@ public final class IndicesPermission {
      *
      * @return a map of all index and privilege pattern automatons
      */
-    private Map<Automaton, Automaton> indexGroupAutomatons(boolean combine) {
+    private Map<Automaton, Automaton> indexGroupAutomatons(boolean combine, IndexComponentSelector selector) {
         // Map of privilege automaton object references (cached by IndexPrivilege::CACHE)
         Map<Automaton, Automaton> allAutomatons = new HashMap<>();
         for (Group group : groups) {
-            // TODO support failure store privileges
-            // we also check that the group does not support data access to avoid erroneously filtering out `all` privilege groups
-            if (group.checkSelector(IndexComponentSelector.FAILURES) && false == group.checkSelector(IndexComponentSelector.DATA)) {
+            if (false == group.checkSelector(selector)) {
                 continue;
             }
             Automaton indexAutomaton = group.getIndexMatcherAutomaton();
@@ -845,6 +865,41 @@ public final class IndicesPermission {
         return allAutomatons;
     }
 
+    private static boolean containsPrivilegesForFailuresSelector(Set<String> checkForPrivileges) {
+        for (String privilege : checkForPrivileges) {
+            // use `getNamedOrNull` since only a named privilege can be a failures-only privilege (raw action names are always data access)
+            IndexPrivilege named = IndexPrivilege.getNamedOrNull(privilege);
+            // note: we are looking for failures-only privileges here, not `all` which does cover failures but is not a failures-only
+            // privilege
+            if (named != null && named.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    private static Automaton getIndexPrivilegesAutomaton(Map<Automaton, Automaton> indexGroupAutomatons, Automaton checkIndexAutomaton) {
+        if (indexGroupAutomatons.isEmpty()) {
+            return null;
+        }
+        Automaton allowedPrivilegesAutomaton = null;
+        for (Map.Entry<Automaton, Automaton> indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) {
+            Automaton indexNameAutomaton = indexAndPrivilegeAutomaton.getValue();
+            if (Operations.subsetOf(checkIndexAutomaton, indexNameAutomaton)) {
+                Automaton privilegesAutomaton = indexAndPrivilegeAutomaton.getKey();
+                if (allowedPrivilegesAutomaton != null) {
+                    allowedPrivilegesAutomaton = Automatons.unionAndMinimize(
+                        Arrays.asList(allowedPrivilegesAutomaton, privilegesAutomaton)
+                    );
+                } else {
+                    allowedPrivilegesAutomaton = privilegesAutomaton;
+                }
+            }
+        }
+        return allowedPrivilegesAutomaton;
+    }
+
     public static class Group {
         public static final Group[] EMPTY_ARRAY = new Group[0];
 

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

@@ -221,6 +221,660 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase {
             }""");
     }
 
+    public void testHasPrivileges() throws IOException {
+        createUser("user", PASSWORD, "role");
+
+        upsertRole("""
+            {
+              "cluster": ["all"],
+              "indices": [
+                {
+                  "names": ["*"],
+                  "privileges": ["read", "read_failure_store"]
+                },
+                {
+                  "names": ["test2"],
+                  "privileges": ["manage_failure_store", "write"]
+                }
+              ]
+            }
+            """, "role");
+        createAndStoreApiKey("user", randomBoolean() ? null : """
+            {
+              "role": {
+                "cluster": ["all"],
+                "indices": [
+                  {
+                    "names": ["*"],
+                    "privileges": ["read", "read_failure_store"]
+                  },
+                  {
+                    "names": ["test2"],
+                    "privileges": ["manage_failure_store", "write"]
+                  }
+                ]
+              }
+            }
+            """);
+
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read", "read_failure_store"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["read"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["read_failure_store"]
+                    },
+                    {
+                        "names": ["test1"],
+                        "privileges": ["manage_failure_store"]
+                    },
+                    {
+                        "names": ["test1"],
+                        "privileges": ["manage"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["manage_failure_store"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["manage"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read": true,
+                        "read_failure_store": true,
+                        "manage_failure_store": false,
+                        "manage": false
+                    },
+                    "test2": {
+                        "read": true,
+                        "read_failure_store": true,
+                        "manage_failure_store": true,
+                        "manage": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["indices:data/write/*"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["indices:admin/*", "indices:data/write/*"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "indices:data/write/*": false
+                    },
+                    "test2": {
+                        "indices:admin/*": false,
+                        "indices:data/write/*": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["indices:data/write/*"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "indices:data/write/*": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": true,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read_failure_store"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": true,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read_failure_store": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": [".security-7"],
+                        "privileges": ["read_failure_store"],
+                        "allow_restricted_indices": true
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    ".security-7": {
+                        "read_failure_store": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": [".security-7", "test1"],
+                        "privileges": ["read_failure_store"],
+                        "allow_restricted_indices": true
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    ".security-7": {
+                        "read_failure_store": false
+                    },
+                    "test1": {
+                        "read_failure_store": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+
+        upsertRole("""
+            {
+              "cluster": ["all"],
+              "indices": [
+                {
+                  "names": ["*"],
+                  "privileges": ["indices:data/read/*"]
+                },
+                {
+                  "names": ["test*"],
+                  "privileges": ["read_failure_store"]
+                },
+                {
+                  "names": ["test2"],
+                  "privileges": ["all"]
+                }
+              ]
+            }
+            """, "role");
+        apiKeys.remove("user");
+        createAndStoreApiKey("user", randomBoolean() ? null : """
+            {
+                "role": {
+                  "cluster": ["all"],
+                  "indices": [
+                    {
+                      "names": ["*"],
+                      "privileges": ["indices:data/read/*"]
+                    },
+                    {
+                      "names": ["test*"],
+                      "privileges": ["read_failure_store"]
+                    },
+                    {
+                      "names": ["test2"],
+                      "privileges": ["all"]
+                    }
+                  ]
+                }
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["all", "indices:data/read/*", "read", "read_failure_store", "write"]
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["all", "indices:data/read/*", "read", "read_failure_store", "write"]
+                    },
+                    {
+                        "names": ["test3"],
+                        "privileges": ["all", "indices:data/read/*", "read", "read_failure_store", "write"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "all": false,
+                        "indices:data/read/*": true,
+                        "read": false,
+                        "read_failure_store": true,
+                        "write": false
+                    },
+                    "test2": {
+                        "all": true,
+                        "indices:data/read/*": true,
+                        "read": true,
+                        "read_failure_store": true,
+                        "write": true
+                    },
+                    "test3": {
+                        "all": false,
+                        "indices:data/read/*": true,
+                        "read": false,
+                        "read_failure_store": true,
+                        "write": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+
+        upsertRole("""
+            {
+              "cluster": ["all"],
+              "indices": [
+                {
+                  "names": ["test1"],
+                  "privileges": ["read", "read_failure_store"]
+                }
+              ]
+            }
+            """, "role");
+        apiKeys.remove("user");
+        createAndStoreApiKey("user", randomBoolean() ? null : """
+            {
+                "role": {
+                  "cluster": ["all"],
+                  "indices": [
+                    {
+                      "names": ["test1"],
+                      "privileges": ["read", "read_failure_store"]
+                    }
+                  ]
+                }
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["all"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "all": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+
+        upsertRole("""
+            {
+              "cluster": ["all"],
+              "indices": [
+                {
+                  "names": ["test1"],
+                  "privileges": ["all"]
+                }
+              ]
+            }
+            """, "role");
+        apiKeys.remove("user");
+        createAndStoreApiKey("user", randomBoolean() ? null : """
+            {
+                "role": {
+                  "cluster": ["all"],
+                  "indices": [
+                    {
+                      "names": ["test1"],
+                      "privileges": ["all"]
+                    }
+                  ]
+                }
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["all"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": true,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "all": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": true,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read_failure_store"]
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": true,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read_failure_store": true
+                    }
+                },
+                "application": {}
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": [".security-7"],
+                        "privileges": ["read_failure_store", "read", "all"],
+                        "allow_restricted_indices": true
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    ".security-7": {
+                        "read_failure_store": false,
+                        "read": false,
+                        "all": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+
+        upsertRole("""
+            {
+              "cluster": ["all"],
+              "indices": [
+                {
+                  "names": [".*"],
+                  "privileges": ["read_failure_store"],
+                  "allow_restricted_indices": true
+                },
+                {
+                  "names": [".*"],
+                  "privileges": ["read"],
+                  "allow_restricted_indices": false
+                }
+              ]
+            }
+            """, "role");
+        apiKeys.remove("user");
+        createAndStoreApiKey("user", randomBoolean() ? null : """
+            {
+                "role": {
+                    "cluster": ["all"],
+                    "indices": [
+                        {
+                            "names": [".*"],
+                            "privileges": ["read_failure_store"],
+                            "allow_restricted_indices": true
+                        },
+                        {
+                            "names": [".*"],
+                            "privileges": ["read"],
+                            "allow_restricted_indices": false
+                        }
+                    ]
+                }
+            }
+            """);
+        expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": [".security-7"],
+                        "privileges": ["read_failure_store", "read", "all"],
+                        "allow_restricted_indices": true
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    ".security-7": {
+                        "read_failure_store": true,
+                        "read": false,
+                        "all": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+
+        // invalid payloads with explicit selectors in index patterns
+        expectThrows(() -> expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1", "test1::failures"],
+                        "privileges": ["read_failure_store", "read", "all"],
+                        "allow_restricted_indices": false
+                    }
+                ]
+            }
+            """, """
+            {}
+            """), 400);
+        expectThrows(() -> expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1::data"],
+                        "privileges": ["read_failure_store", "read", "all"],
+                        "allow_restricted_indices": false
+                    }
+                ]
+            }
+            """, """
+            {}
+            """), 400);
+        expectThrows(() -> expectHasPrivileges("user", """
+            {
+                "index": [
+                    {
+                        "names": ["test1::failures"],
+                        "privileges": ["read_failure_store", "read", "all"],
+                        "allow_restricted_indices": false
+                    }
+                ]
+            }
+            """, """
+            {}
+            """), 400);
+    }
+
+    public void testHasPrivilegesWithApiKeys() throws IOException {
+        var user = "user";
+        var role = "role";
+        createUser(user, PASSWORD, role);
+        upsertRole("""
+            {
+                "cluster": ["all"],
+                "indices": [
+                    {
+                        "names": ["*"],
+                        "privileges": ["read_failure_store"]
+                    }
+                ]
+            }
+            """, role);
+
+        String apiKey = createApiKey(user, """
+            {
+                "role": {
+                    "cluster": ["all"],
+                    "indices": [{"names": ["test1"], "privileges": ["read_failure_store"]}]
+                }
+            }""");
+
+        expectHasPrivilegesWithApiKey(apiKey, """
+            {
+                "index": [
+                    {
+                        "names": ["test1"],
+                        "privileges": ["read_failure_store"],
+                        "allow_restricted_indices": true
+                    },
+                    {
+                        "names": ["test2"],
+                        "privileges": ["read_failure_store"],
+                        "allow_restricted_indices": true
+                    }
+                ]
+            }
+            """, """
+            {
+                "username": "user",
+                "has_all_requested": false,
+                "cluster": {},
+                "index": {
+                    "test1": {
+                        "read_failure_store": true
+                    },
+                    "test2": {
+                        "read_failure_store": false
+                    }
+                },
+                "application": {}
+            }
+            """);
+    }
+
     public void testRoleWithSelectorInIndexPattern() throws Exception {
         setupDataStream();
 
@@ -281,7 +935,6 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase {
         expectSearch("user", new Search("*::failures"));
     }
 
-    @SuppressWarnings("unchecked")
     public void testFailureStoreAccess() throws Exception {
         List<String> docIds = setupDataStream();
         assertThat(docIds.size(), equalTo(2));
@@ -1792,6 +2445,11 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase {
         return client().performRequest(request);
     }
 
+    private Response performRequestWithRunAs(String user, Request request) throws IOException {
+        request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user).build());
+        return adminClient().performRequest(request);
+    }
+
     private Response performRequestMaybeUsingApiKey(String user, Request request) throws IOException {
         if (randomBoolean() && apiKeys.containsKey(user)) {
             return performRequestWithApiKey(apiKeys.get(user), request);
@@ -1920,4 +2578,18 @@ public class FailureStoreSecurityRestIT extends ESRestTestCase {
         assertThat(indices.v2().size(), equalTo(1));
         return new Tuple<>(indices.v1().get(0), indices.v2().get(0));
     }
+
+    private void expectHasPrivileges(String user, String requestBody, String expectedResponse) throws IOException {
+        Request req = new Request("POST", "/_security/user/_has_privileges");
+        req.setJsonEntity(requestBody);
+        Response response = randomBoolean() ? performRequestMaybeUsingApiKey(user, req) : performRequestWithRunAs(user, req);
+        assertThat(responseAsMap(response), equalTo(mapFromJson(expectedResponse)));
+    }
+
+    private void expectHasPrivilegesWithApiKey(String apiKey, String requestBody, String expectedResponse) throws IOException {
+        Request req = new Request("POST", "/_security/user/_has_privileges");
+        req.setJsonEntity(requestBody);
+        Response response = performRequestWithApiKey(apiKey, req);
+        assertThat(responseAsMap(response), equalTo(mapFromJson(expectedResponse)));
+    }
 }

+ 44 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.security.action.user;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.cluster.metadata.DataStream;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
@@ -90,6 +91,49 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
         assertThat(ile.getMessage(), containsString("may only check index privileges without any DLS query"));
     }
 
+    public void testHasPrivilegesRequestDoesNotAllowSelectorsInIndexPatterns() {
+        assumeTrue("failure store required", DataStream.isFailureStoreFeatureFlagEnabled());
+
+        final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
+        final SecurityContext context = mock(SecurityContext.class);
+        final User user = new User("user-1", "superuser");
+        final Authentication authentication = AuthenticationTestHelper.builder()
+            .user(user)
+            .realmRef(new Authentication.RealmRef("native", "default_native", "node1"))
+            .build(false);
+        when(context.getAuthentication()).thenReturn(authentication);
+        threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
+
+        TransportService transportService = MockUtils.setupTransportServiceWithThreadpoolExecutor();
+        final TransportHasPrivilegesAction transportHasPrivilegesAction = new TransportHasPrivilegesAction(
+            transportService,
+            new ActionFilters(Set.of()),
+            mock(AuthorizationService.class),
+            mock(NativePrivilegeStore.class),
+            context
+        );
+
+        final HasPrivilegesRequest request = new HasPrivilegesRequest();
+        final RoleDescriptor.IndicesPrivileges[] indicesPrivileges = new RoleDescriptor.IndicesPrivileges[randomIntBetween(1, 5)];
+        for (int i = 0; i < indicesPrivileges.length; i++) {
+            indicesPrivileges[i] = RoleDescriptor.IndicesPrivileges.builder()
+                .privileges(randomFrom("read", "write"))
+                .indices(randomAlphaOfLengthBetween(2, 8) + randomFrom("::failures", "::data"))
+                .build();
+        }
+        request.indexPrivileges(indicesPrivileges);
+        request.clusterPrivileges(new String[0]);
+        request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]);
+        request.username("user-1");
+
+        final PlainActionFuture<HasPrivilegesResponse> listener = new PlainActionFuture<>();
+        transportHasPrivilegesAction.execute(mock(Task.class), request, listener);
+
+        final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, listener::actionGet);
+        assertThat(ile, notNullValue());
+        assertThat(ile.getMessage(), containsString("may only check index privileges without selectors in index patterns"));
+    }
+
     public void testRequiresSameUser() {
         final SecurityContext context = mock(SecurityContext.class);