diff --git a/docs/changelog/96479.yaml b/docs/changelog/96479.yaml new file mode 100644 index 0000000000000..57d290dce0fd3 --- /dev/null +++ b/docs/changelog/96479.yaml @@ -0,0 +1,6 @@ +pr: 96479 +summary: Resolving wildcard application names without prefix query +area: Authorization +type: bug +issues: + - 96465 diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreSingleNodeTests.java new file mode 100644 index 0000000000000..c565aa925f5aa --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreSingleNodeTests.java @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz.store; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.role.PutRoleAction; +import org.elasticsearch.xpack.core.security.action.role.PutRoleRequestBuilder; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; +import org.elasticsearch.xpack.core.security.action.user.PutUserAction; +import org.elasticsearch.xpack.core.security.action.user.PutUserRequestBuilder; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.junit.Before; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; +import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD; +import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class NativePrivilegeStoreSingleNodeTests extends SecuritySingleNodeTestCase { + + @Before + public void configureApplicationPrivileges() { + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(); + final List applicationPrivilegeDescriptors = Arrays.asList( + new ApplicationPrivilegeDescriptor("myapp-1", "read", Set.of("action:read"), emptyMap()), + new ApplicationPrivilegeDescriptor("myapp-1", "write", Set.of("action:write"), emptyMap()), + new ApplicationPrivilegeDescriptor("myapp-2", "read", Set.of("action:read"), emptyMap()), + new ApplicationPrivilegeDescriptor("myapp-2", "write", Set.of("action:write"), emptyMap()), + new ApplicationPrivilegeDescriptor("yourapp-1", "read", Set.of("action:read"), emptyMap()), + new ApplicationPrivilegeDescriptor("yourapp-1", "write", Set.of("action:write"), emptyMap()), + new ApplicationPrivilegeDescriptor("yourapp-2", "read", Set.of("action:read"), emptyMap()), + new ApplicationPrivilegeDescriptor("yourapp-2", "write", Set.of("action:write"), emptyMap()), + new ApplicationPrivilegeDescriptor("randomapp", "read", Set.of("action:read"), emptyMap()), + new ApplicationPrivilegeDescriptor("randomapp", "write", Set.of("action:write"), emptyMap()) + ); + putPrivilegesRequest.setPrivileges(applicationPrivilegeDescriptors); + client().execute(PutPrivilegesAction.INSTANCE, putPrivilegesRequest).actionGet(); + } + + public void testResolvePrivilegesWorkWhenExpensiveQueriesAreDisabled() throws IOException { + // Disable expensive query + new ClusterUpdateSettingsRequestBuilder(client(), ClusterUpdateSettingsAction.INSTANCE).setTransientSettings( + Settings.builder().put(ALLOW_EXPENSIVE_QUERIES.getKey(), false) + ).execute().actionGet(); + + try { + // Prove that expensive queries are indeed disabled + final ActionFuture future = client().prepareSearch(".security") + .setQuery(QueryBuilders.prefixQuery("application", "my")) + .execute(); + final ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); + assertThat( + e.getCause().getMessage(), + containsString("[prefix] queries cannot be executed when 'search.allow_expensive_queries' is set to false") + ); + + // Get privileges work with wildcard application name + final GetPrivilegesResponse getPrivilegesResponse = new GetPrivilegesRequestBuilder(client()).application("yourapp*") + .execute() + .actionGet(); + assertThat(getPrivilegesResponse.privileges(), arrayWithSize(4)); + assertThat( + Arrays.stream(getPrivilegesResponse.privileges()) + .map(ApplicationPrivilegeDescriptor::getApplication) + .collect(Collectors.toUnmodifiableSet()), + containsInAnyOrder("yourapp-1", "yourapp-2") + ); + + // User role resolution works with wildcard application name + new PutRoleRequestBuilder(client(), PutRoleAction.INSTANCE).source("app_user_role", new BytesArray(""" + { + "cluster": ["manage_own_api_key"], + "applications": [ + { + "application": "myapp-*", + "privileges": ["read"], + "resources": ["shared*"] + }, + { + "application": "yourapp-1", + "privileges": ["read", "write"], + "resources": ["public"] + } + ] + } + """), XContentType.JSON).execute().actionGet(); + + new PutUserRequestBuilder(client(), PutUserAction.INSTANCE).username("app_user") + .password(TEST_PASSWORD_SECURE_STRING, getFastStoredHashAlgoForTests()) + .roles("app_user_role") + .execute() + .actionGet(); + + Client appUserClient; + appUserClient = client().filterWithHeader( + Map.of( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString(("app_user:" + TEST_PASSWORD).getBytes(StandardCharsets.UTF_8)) + ) + ); + if (randomBoolean()) { + final var createApiKeyRequest = new CreateApiKeyRequest(); + createApiKeyRequest.setName(randomAlphaOfLength(5)); + if (randomBoolean()) { + createApiKeyRequest.setRoleDescriptors( + List.of( + new RoleDescriptor( + randomAlphaOfLength(5), + null, + null, + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("myapp-*") + .privileges("read") + .resources("shared-common-*") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("yourapp-*") + .privileges("write") + .resources("public") + .build() }, + null, + null, + null, + null + ) + ) + ); + } + final CreateApiKeyResponse createApiKeyResponse = appUserClient.execute(CreateApiKeyAction.INSTANCE, createApiKeyRequest) + .actionGet(); + appUserClient = client().filterWithHeader( + Map.of( + "Authorization", + "ApiKey " + + Base64.getEncoder() + .encodeToString( + (createApiKeyResponse.getId() + ":" + createApiKeyResponse.getKey()).getBytes(StandardCharsets.UTF_8) + ) + ) + ); + } + + final AuthenticateResponse authenticateResponse = appUserClient.execute( + AuthenticateAction.INSTANCE, + AuthenticateRequest.INSTANCE + ).actionGet(); + assertThat(authenticateResponse.authentication().getEffectiveSubject().getUser().principal(), equalTo("app_user")); + } finally { + // Reset setting since test suite expects things in a clean slate + new ClusterUpdateSettingsRequestBuilder(client(), ClusterUpdateSettingsAction.INSTANCE).setTransientSettings( + Settings.builder().putNull(ALLOW_EXPENSIVE_QUERIES.getKey()) + ).execute().actionGet(); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index fcb9fd8bb8bb1..dda471a8a50f7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -759,7 +759,8 @@ Collection createComponents( settings, client, systemIndices.getMainIndexManager(), - cacheInvalidatorRegistry + cacheInvalidatorRegistry, + clusterService ); components.add(privilegeStore); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java index f97bdc926f8b1..592fa16b79ff7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java @@ -18,6 +18,7 @@ import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; @@ -28,6 +29,7 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.query.BoolQueryBuilder; @@ -47,6 +49,7 @@ import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheResponse; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.support.StringMatcher; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.LockingAtomicCounter; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -59,11 +62,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Collectors; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; @@ -103,17 +108,21 @@ public class NativePrivilegeStore { private final Settings settings; private final Client client; private final SecurityIndexManager securityIndexManager; + private volatile boolean allowExpensiveQueries; private final DescriptorsAndApplicationNamesCache descriptorsAndApplicationNamesCache; public NativePrivilegeStore( Settings settings, Client client, SecurityIndexManager securityIndexManager, - CacheInvalidatorRegistry cacheInvalidatorRegistry + CacheInvalidatorRegistry cacheInvalidatorRegistry, + ClusterService clusterService ) { this.settings = settings; this.client = client; this.securityIndexManager = securityIndexManager; + this.allowExpensiveQueries = ALLOW_EXPENSIVE_QUERIES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(ALLOW_EXPENSIVE_QUERIES, this::setAllowExpensiveQueries); final TimeValue ttl = CACHE_TTL_SETTING.get(settings); if (ttl.getNanos() > 0) { descriptorsAndApplicationNamesCache = new DescriptorsAndApplicationNamesCache( @@ -193,7 +202,16 @@ private void innerGetPrivileges(Collection applications, ActionListener< ApplicationPrivilegeDescriptor.Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE ); - final QueryBuilder query = QueryBuilders.boolQuery().filter(typeQuery).filter(getApplicationNameQuery(applications)); + final Tuple> applicationNameQueryAndPredicate = getApplicationNameQueryAndPredicate( + applications + ); + + final QueryBuilder query; + if (applicationNameQueryAndPredicate.v1() != null) { + query = QueryBuilders.boolQuery().filter(typeQuery).filter(applicationNameQueryAndPredicate.v1()); + } else { + query = QueryBuilders.boolQuery().filter(typeQuery); + } final Supplier supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { @@ -209,16 +227,16 @@ private void innerGetPrivileges(Collection applications, ActionListener< client, request, new ContextPreservingActionListener<>(supplier, listener), - hit -> buildPrivilege(hit.getId(), hit.getSourceRef()) + hit -> buildPrivilege(hit.getId(), hit.getSourceRef(), applicationNameQueryAndPredicate.v2()) ); } }); } } - private static QueryBuilder getApplicationNameQuery(Collection applications) { + private Tuple> getApplicationNameQueryAndPredicate(Collection applications) { if (applications.contains("*")) { - return QueryBuilders.existsQuery(APPLICATION.getPreferredName()); + return new Tuple<>(QueryBuilders.existsQuery(APPLICATION.getPreferredName()), null); } final List rawNames = new ArrayList<>(applications.size()); final List wildcardNames = new ArrayList<>(applications.size()); @@ -234,26 +252,43 @@ private static QueryBuilder getApplicationNameQuery(Collection applicati TermsQueryBuilder termsQuery = rawNames.isEmpty() ? null : QueryBuilders.termsQuery(APPLICATION.getPreferredName(), rawNames); if (wildcardNames.isEmpty()) { - return termsQuery; + return new Tuple<>(termsQuery, null); } - final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); - if (termsQuery != null) { - boolQuery.should(termsQuery); - } - for (String wildcard : wildcardNames) { - final String prefix = wildcard.substring(0, wildcard.length() - 1); - boolQuery.should(QueryBuilders.prefixQuery(APPLICATION.getPreferredName(), prefix)); + + if (allowExpensiveQueries) { + final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + if (termsQuery != null) { + boolQuery.should(termsQuery); + } + for (String wildcard : wildcardNames) { + final String prefix = wildcard.substring(0, wildcard.length() - 1); + boolQuery.should(QueryBuilders.prefixQuery(APPLICATION.getPreferredName(), prefix)); + } + boolQuery.minimumShouldMatch(1); + return new Tuple<>(boolQuery, null); + } else { + logger.trace("expensive queries are not allowed, switching to filtering application names in memory"); + return new Tuple<>(null, StringMatcher.of(applications)); } - boolQuery.minimumShouldMatch(1); - return boolQuery; } - private static ApplicationPrivilegeDescriptor buildPrivilege(String docId, BytesReference source) { + private void setAllowExpensiveQueries(boolean allowExpensiveQueries) { + this.allowExpensiveQueries = allowExpensiveQueries; + } + + private static ApplicationPrivilegeDescriptor buildPrivilege( + String docId, + BytesReference source, + @Nullable Predicate applicationNamePredicate + ) { logger.trace("Building privilege from [{}] [{}]", docId, source == null ? "<>" : source.utf8ToString()); if (source == null) { return null; } final Tuple name = nameFromDocId(docId); + if (applicationNamePredicate != null && false == applicationNamePredicate.test(name.v1())) { + return null; + } try { // EMPTY is safe here because we never use namedObject diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java index 000b4f725f7ef..10ed5c66f3c15 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java @@ -18,22 +18,28 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponseSections; +import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; +import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheRequest; @@ -69,6 +75,7 @@ import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.elasticsearch.common.util.set.Sets.newHashSet; +import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.contains; @@ -93,6 +100,9 @@ public class NativePrivilegeStoreTests extends ESTestCase { private Client client; private SecurityIndexManager securityIndex; private CacheInvalidatorRegistry cacheInvalidatorRegistry; + private ThreadPool threadPool; + private ClusterService clusterService; + private boolean allowExpensiveQueries; @Before public void setup() { @@ -109,6 +119,11 @@ protected void NativePrivilegeStoreTests.this.requests.add(request); NativePrivilegeStoreTests.this.listener.set((ActionListener) listener); } + + @Override + public void searchScroll(SearchScrollRequest request, ActionListener listener) { + listener.onResponse(SearchResponse.empty(() -> 1L, SearchResponse.Clusters.EMPTY)); + } }; securityIndex = mock(SecurityIndexManager.class); when(securityIndex.freeze()).thenReturn(securityIndex); @@ -127,12 +142,24 @@ protected void return null; }).when(securityIndex).checkIndexVersionThenExecute(anyConsumer(), any(Runnable.class)); cacheInvalidatorRegistry = new CacheInvalidatorRegistry(); - store = new NativePrivilegeStore(Settings.EMPTY, client, securityIndex, cacheInvalidatorRegistry); + + threadPool = new TestThreadPool(getTestName()); + final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + clusterService = ClusterServiceUtils.createClusterService(threadPool, clusterSettings); + allowExpensiveQueries = randomBoolean(); + store = new NativePrivilegeStore( + setAllowExpensiveQueries(Settings.EMPTY), + client, + securityIndex, + cacheInvalidatorRegistry, + clusterService + ); } @After public void cleanup() { client.close(); + terminate(threadPool); } public void testGetSinglePrivilegeByName() throws Exception { @@ -255,6 +282,39 @@ public void testGetPrivilegesByApplicationName() throws Exception { } public void testGetPrivilegesByWildcardApplicationName() throws Exception { + final List sourcePrivileges = List.of( + new ApplicationPrivilegeDescriptor( + "theapp", + randomAlphaOfLength(5), + newHashSet("action:" + randomAlphaOfLength(5) + "/*"), + emptyMap() + ), + new ApplicationPrivilegeDescriptor( + "myapp-1", + randomAlphaOfLength(5), + newHashSet("action:" + randomAlphaOfLength(5) + "/*"), + emptyMap() + ), + new ApplicationPrivilegeDescriptor( + "myapp-2", + randomAlphaOfLength(5), + newHashSet("action:" + randomAlphaOfLength(5) + "/*"), + emptyMap() + ), + new ApplicationPrivilegeDescriptor( + "yourapp", + randomAlphaOfLength(5), + newHashSet("action:" + randomAlphaOfLength(5) + "/*"), + emptyMap() + ), + new ApplicationPrivilegeDescriptor( + "theirapp", + randomAlphaOfLength(5), + newHashSet("action:" + randomAlphaOfLength(5) + "/*"), + emptyMap() + ) + ); + final PlainActionFuture> future = new PlainActionFuture<>(); store.getPrivileges(Arrays.asList("myapp-*", "yourapp"), null, future); assertThat(requests, iterableWithSize(1)); @@ -263,11 +323,18 @@ public void testGetPrivilegesByWildcardApplicationName() throws Exception { assertThat(request.indices(), arrayContaining(SecuritySystemIndices.SECURITY_MAIN_ALIAS)); final String query = Strings.toString(request.source().query()); - assertThat(query, containsString("{\"bool\":{\"should\":[{\"terms\":{\"application\":[\"yourapp\"]")); - assertThat(query, containsString("{\"prefix\":{\"application\":{\"value\":\"myapp-\"")); - assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\"")); + if (allowExpensiveQueries) { + assertThat(query, containsString("{\"bool\":{\"should\":[{\"terms\":{\"application\":[\"yourapp\"]")); + assertThat(query, containsString("{\"prefix\":{\"application\":{\"value\":\"myapp-\"")); + assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\"")); + } else { + assertThat( + query, + equalTo("{\"bool\":{\"filter\":[{\"term\":{\"type\":{\"value\":\"application-privilege\"}}}],\"boost\":1.0}}") + ); + } - final SearchHit[] hits = new SearchHit[0]; + final SearchHit[] hits = buildHits(allowExpensiveQueries ? sourcePrivileges.subList(1, 4) : sourcePrivileges); listener.get() .onResponse( new SearchResponse( @@ -289,6 +356,8 @@ public void testGetPrivilegesByWildcardApplicationName() throws Exception { null ) ); + // The first and last privilege should not be retrieved + assertResult(sourcePrivileges.subList(1, 4), future); } public void testGetPrivilegesByStarApplicationName() throws Exception { @@ -608,10 +677,11 @@ public void testWhenStaleResultsAreCachedTheyWillBeCleared() throws InterruptedE // This simulates the scenario when stale results are cached just before the invalidation call arrives. // In this case, we guarantee the cache will be invalidate and the stale results won't stay for long. final NativePrivilegeStore store1 = new NativePrivilegeStore( - Settings.EMPTY, + setAllowExpensiveQueries(Settings.EMPTY), client, securityIndex, - new CacheInvalidatorRegistry() + new CacheInvalidatorRegistry(), + clusterService ) { @Override protected void cacheFetchedDescriptors( @@ -739,10 +809,11 @@ public void testRetrieveActionNamePatternsInsteadOfPrivileges() throws Exception Settings.EMPTY ); NativePrivilegeStore store1 = new NativePrivilegeStore( - settings, + setAllowExpensiveQueries(settings), mockClient, mockSecurityIndexManager, - new CacheInvalidatorRegistry() + new CacheInvalidatorRegistry(), + clusterService ); store1.getPrivileges(applications, actions, future); assertResult(emptyList(), future); @@ -843,14 +914,26 @@ public void testCacheClearOnIndexHealthChange() { public void testCacheWillBeDisabledWhenTtlIsZero() { final Settings settings = Settings.builder().put("xpack.security.authz.store.privileges.cache.ttl", 0).build(); - final NativePrivilegeStore store1 = new NativePrivilegeStore(settings, client, securityIndex, new CacheInvalidatorRegistry()); + final NativePrivilegeStore store1 = new NativePrivilegeStore( + settings, + client, + securityIndex, + new CacheInvalidatorRegistry(), + clusterService + ); assertNull(store1.getApplicationNamesCache()); assertNull(store1.getDescriptorsCache()); } public void testGetPrivilegesWorkWithoutCache() throws Exception { final Settings settings = Settings.builder().put("xpack.security.authz.store.privileges.cache.ttl", 0).build(); - final NativePrivilegeStore store1 = new NativePrivilegeStore(settings, client, securityIndex, new CacheInvalidatorRegistry()); + final NativePrivilegeStore store1 = new NativePrivilegeStore( + settings, + client, + securityIndex, + new CacheInvalidatorRegistry(), + clusterService + ); assertNull(store1.getDescriptorsAndApplicationNamesCache()); final List sourcePrivileges = Arrays.asList( new ApplicationPrivilegeDescriptor("myapp", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()) @@ -925,4 +1008,8 @@ private void assertResult( private static Consumer anyConsumer() { return any(Consumer.class); } + + private Settings setAllowExpensiveQueries(Settings settings) { + return Settings.builder().put(settings).put(ALLOW_EXPENSIVE_QUERIES.getKey(), allowExpensiveQueries).build(); + } }