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 a718b1dee04ce..0c8152e304550 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 @@ -340,14 +340,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) { @@ -369,31 +382,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 3ad3f6fa4f7a6..8d113e35c8e68 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 (Automatons.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 - && Automatons.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 bfdea27eee98c..5800ed25712aa 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 @@ -220,6 +220,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(); @@ -280,7 +934,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)); @@ -1699,6 +2352,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); @@ -1827,4 +2485,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);