From 666f66c67443b7c44d9a49889a8f1d5b0bfcd269 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 26 Mar 2025 13:20:57 +0100 Subject: [PATCH 1/3] [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 --- .../security/authz/AuthorizationEngine.java | 42 +- .../authz/permission/IndicesPermission.java | 101 ++- .../FailureStoreSecurityRestIT.java | 674 +++++++++++++++++- .../TransportHasPrivilegesActionTests.java | 44 ++ 4 files changed, 810 insertions(+), 51 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 0eb0043ae0baf..0912ddc9ec4a6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -339,14 +339,27 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio 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 ActionRequestValidationException validate(ActionRequestValidationExceptio && 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; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 47f0751fe1876..9d65a1b59d3d1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -317,36 +317,58 @@ public boolean checkResourcePrivileges( @Nullable ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder ) { boolean allMatch = true; - Map indexGroupAutomatons = indexGroupAutomatons( - combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex) + Map 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 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 + && Automatons.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForDataSelector)) { if (resourcePrivilegesMapBuilder != null) { resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE); } - } else { + } else if (checkWithFailuresSelector + && allowedPrivilegesAutomatonForFailuresSelector != null + && Automatons.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 @@ private static boolean containsPrivilegeThatGrantsMappingUpdatesForBwc(Group gro * * @return a map of all index and privilege pattern automatons */ - private Map indexGroupAutomatons(boolean combine) { + private Map indexGroupAutomatons(boolean combine, IndexComponentSelector selector) { // Map of privilege automaton object references (cached by IndexPrivilege::CACHE) Map 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 @@ private Map indexGroupAutomatons(boolean combine) { return allAutomatons; } + private static boolean containsPrivilegesForFailuresSelector(Set 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 indexGroupAutomatons, Automaton checkIndexAutomaton) { + if (indexGroupAutomatons.isEmpty()) { + return null; + } + Automaton allowedPrivilegesAutomaton = null; + for (Map.Entry indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) { + Automaton indexNameAutomaton = indexAndPrivilegeAutomaton.getValue(); + if (Automatons.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]; diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index f54ec41e37eb0..85b5fd6d82401 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -221,6 +221,660 @@ public void testGetUserPrivileges() throws IOException { }"""); } + 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 void testRoleWithSelectorInIndexPattern() throws Exception { expectSearch("user", new Search("*::failures")); } - @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws Exception { List docIds = setupDataStream(); assertThat(docIds.size(), equalTo(2)); @@ -1792,6 +2445,11 @@ private Response performRequest(String user, Request request) throws IOException 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 @@ private Tuple getSingleDataAndFailureIndices(String dataStreamNa 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))); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index 9f7815a1c9891..6f9ac7fffc215 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -9,6 +9,7 @@ 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 void testHasPrivilegesRequestDoesNotAllowDLSRoleQueryBasedIndicesPrivileg 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 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); From afa7d5c7d1f698ccf65a95f35c190e61ffe0a987 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 31 Mar 2025 13:15:17 +0200 Subject: [PATCH 2/3] fix compilation issue: use Operations.subsetOf --- .../core/security/authz/permission/IndicesPermission.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 9d65a1b59d3d1..8b7b0cf63b89f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -356,13 +356,13 @@ public boolean checkResourcePrivileges( final Automaton automatonToCheck = indexPrivilege.getAutomaton(); if (checkWithDataSelector && allowedPrivilegesAutomatonForDataSelector != null - && Automatons.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForDataSelector)) { + && Operations.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForDataSelector)) { if (resourcePrivilegesMapBuilder != null) { resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE); } } else if (checkWithFailuresSelector && allowedPrivilegesAutomatonForFailuresSelector != null - && Automatons.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForFailuresSelector)) { + && Operations.subsetOf(automatonToCheck, allowedPrivilegesAutomatonForFailuresSelector)) { if (resourcePrivilegesMapBuilder != null) { resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE); } From 3521d78dd335912f9d19bf5cfe3978f2a38de48c Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 31 Mar 2025 13:25:49 +0200 Subject: [PATCH 3/3] fix one more place --- .../xpack/core/security/authz/permission/IndicesPermission.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 8b7b0cf63b89f..c2a549ca36327 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -886,7 +886,7 @@ private static Automaton getIndexPrivilegesAutomaton(Map i Automaton allowedPrivilegesAutomaton = null; for (Map.Entry indexAndPrivilegeAutomaton : indexGroupAutomatons.entrySet()) { Automaton indexNameAutomaton = indexAndPrivilegeAutomaton.getValue(); - if (Automatons.subsetOf(checkIndexAutomaton, indexNameAutomaton)) { + if (Operations.subsetOf(checkIndexAutomaton, indexNameAutomaton)) { Automaton privilegesAutomaton = indexAndPrivilegeAutomaton.getKey(); if (allowedPrivilegesAutomaton != null) { allowedPrivilegesAutomaton = Automatons.unionAndMinimize(