-
Notifications
You must be signed in to change notification settings - Fork 24.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolving wildcard application names without prefix query (#96479)
For application privileges, today we use prefix query to resolve application names with trailing wildcard. However, prefix query is considered to be expensive and can be disabled if the cluster setting search.allow_expensive_queries is set to false. When that happens it breaks authorization in a surprising way. This PR adds conditional logic to fallback to in-memory filtering for application names when expensive queries are disabled. It is not less expensive. But it avoids the surprising breakage. Resolves: #96465
- Loading branch information
Showing
5 changed files
with
351 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pr: 96479 | ||
summary: Resolving wildcard application names without prefix query | ||
area: Authorization | ||
type: bug | ||
issues: | ||
- 96465 |
194 changes: 194 additions & 0 deletions
194
...ava/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreSingleNodeTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ApplicationPrivilegeDescriptor> 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<SearchResponse> 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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.