mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-23 05:15:04 +00:00
This commit adds support to retrieve all API keys if the authenticated user is authorized to do so. This removes the restriction of specifying one of the parameters (like id, name, username and/or realm name) when the `owner` is set to `false`. Closes #46887
This commit is contained in:
parent
f93bb9dac5
commit
7c862fe71f
@ -38,13 +38,13 @@ public final class GetApiKeyRequest implements Validatable, ToXContentObject {
|
||||
private final String name;
|
||||
private final boolean ownedByAuthenticatedUser;
|
||||
|
||||
private GetApiKeyRequest() {
|
||||
this(null, null, null, null, false);
|
||||
}
|
||||
|
||||
// pkg scope for testing
|
||||
GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
|
||||
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
|
||||
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
|
||||
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
|
||||
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
|
||||
}
|
||||
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
|
||||
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
|
||||
throwValidationError(
|
||||
@ -147,6 +147,13 @@ public final class GetApiKeyRequest implements Validatable, ToXContentObject {
|
||||
return new GetApiKeyRequest(null, null, null, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates get api key request to retrieve api key information for all api keys if the authenticated user is authorized to do so.
|
||||
*/
|
||||
public static GetApiKeyRequest forAllApiKeys() {
|
||||
return new GetApiKeyRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder;
|
||||
|
@ -1983,6 +1983,18 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
|
||||
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
|
||||
}
|
||||
|
||||
{
|
||||
// tag::get-all-api-keys-request
|
||||
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.forAllApiKeys();
|
||||
// end::get-all-api-keys-request
|
||||
|
||||
GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
|
||||
|
||||
assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue()));
|
||||
assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1));
|
||||
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
|
||||
}
|
||||
|
||||
{
|
||||
// tag::get-user-realm-api-keys-request
|
||||
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("default_file", "test_user");
|
||||
|
@ -52,7 +52,6 @@ public class GetApiKeyRequestTests extends ESTestCase {
|
||||
|
||||
public void testRequestValidationFailureScenarios() throws IOException {
|
||||
String[][] inputs = new String[][] {
|
||||
{ randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), "false" },
|
||||
{ randomNullOrEmptyString(), "user", "api-kid", "api-kname", "false" },
|
||||
{ "realm", randomNullOrEmptyString(), "api-kid", "api-kname", "false" },
|
||||
{ "realm", "user", "api-kid", randomNullOrEmptyString(), "false" },
|
||||
@ -60,7 +59,6 @@ public class GetApiKeyRequestTests extends ESTestCase {
|
||||
{ "realm", randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), "true"},
|
||||
{ randomNullOrEmptyString(), "user", randomNullOrEmptyString(), randomNullOrEmptyString(), "true"} };
|
||||
String[] expectedErrorMessages = new String[] {
|
||||
"One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false",
|
||||
"username or realm name must not be specified when the api key id or api key name is specified",
|
||||
"username or realm name must not be specified when the api key id or api key name is specified",
|
||||
"username or realm name must not be specified when the api key id or api key name is specified",
|
||||
|
@ -23,6 +23,8 @@ The +{request}+ supports retrieving API key information for
|
||||
|
||||
. A specific key or all API keys owned by the current authenticated user
|
||||
|
||||
. All API keys if the user is authorized to do so
|
||||
|
||||
===== Retrieve a specific API key by its id
|
||||
["source","java",subs="attributes,callouts,macros"]
|
||||
--------------------------------------------------
|
||||
@ -59,6 +61,12 @@ include-tagged::{doc-tests-file}[get-user-realm-api-keys-request]
|
||||
include-tagged::{doc-tests-file}[get-api-keys-owned-by-authenticated-user-request]
|
||||
--------------------------------------------------
|
||||
|
||||
===== Retrieve all API keys if the user is authorized to do so
|
||||
["source","java",subs="attributes,callouts,macros"]
|
||||
--------------------------------------------------
|
||||
include-tagged::{doc-tests-file}[get-all-api-keys-request]
|
||||
--------------------------------------------------
|
||||
|
||||
include::../execution.asciidoc[]
|
||||
|
||||
[id="{upid}-{api}-response"]
|
||||
|
@ -51,8 +51,10 @@ by the currently authenticated user. Defaults to false.
|
||||
The 'realm_name' or 'username' parameters cannot be specified when this
|
||||
parameter is set to 'true' as they are assumed to be the currently authenticated ones.
|
||||
|
||||
NOTE: At least one of "id", "name", "username" and "realm_name" must be specified
|
||||
if "owner" is "false" (default).
|
||||
NOTE: When none of the parameters "id", "name", "username" and "realm_name"
|
||||
are specified, and the "owner" is set to false then it will retrieve all API
|
||||
keys if the user is authorized. If the user is not authorized to retrieve other user's
|
||||
API keys, then an error will be returned.
|
||||
|
||||
[[security-api-get-api-key-example]]
|
||||
==== {api-examples-title}
|
||||
@ -123,6 +125,13 @@ GET /_security/api_key?owner=true
|
||||
--------------------------------------------------
|
||||
// TEST[continued]
|
||||
|
||||
The following example retrieves all API keys if the user is authorized to do so:
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
GET /_security/api_key
|
||||
--------------------------------------------------
|
||||
// TEST[continued]
|
||||
|
||||
Following creates an API key
|
||||
|
||||
[source,console]
|
||||
|
@ -133,14 +133,16 @@ public final class GetApiKeyRequest extends ActionRequest {
|
||||
return new GetApiKeyRequest(null, null, null, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates get api key request to retrieve api key information for all api keys if the authenticated user is authorized to do so.
|
||||
*/
|
||||
public static GetApiKeyRequest forAllApiKeys() {
|
||||
return new GetApiKeyRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionRequestValidationException validate() {
|
||||
ActionRequestValidationException validationException = null;
|
||||
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
|
||||
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
|
||||
validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified if " +
|
||||
"[owner] flag is false", validationException);
|
||||
}
|
||||
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
|
||||
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
|
||||
validationException = addValidationError(
|
||||
|
@ -76,8 +76,6 @@ public class GetApiKeyRequestTests extends ESTestCase {
|
||||
}
|
||||
|
||||
String[][] inputs = new String[][]{
|
||||
{randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(),
|
||||
randomNullOrEmptyString(), "false"},
|
||||
{randomNullOrEmptyString(), "user", "api-kid", "api-kname", "false"},
|
||||
{"realm", randomNullOrEmptyString(), "api-kid", "api-kname", "false"},
|
||||
{"realm", "user", "api-kid", randomNullOrEmptyString(), "false"},
|
||||
@ -86,7 +84,6 @@ public class GetApiKeyRequestTests extends ESTestCase {
|
||||
{randomNullOrEmptyString(), "user", randomNullOrEmptyString(), randomNullOrEmptyString(), "true"}
|
||||
};
|
||||
String[][] expectedErrorMessages = new String[][]{
|
||||
{"One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false"},
|
||||
{"username or realm name must not be specified when the api key id or api key name is specified",
|
||||
"only one of [api key id, api key name] can be specified"},
|
||||
{"username or realm name must not be specified when the api key id or api key name is specified",
|
||||
|
@ -888,11 +888,6 @@ public class ApiKeyService {
|
||||
public void getApiKeys(String realmName, String username, String apiKeyName, String apiKeyId,
|
||||
ActionListener<GetApiKeyResponse> listener) {
|
||||
ensureEnabled();
|
||||
if (Strings.hasText(realmName) == false && Strings.hasText(username) == false && Strings.hasText(apiKeyName) == false
|
||||
&& Strings.hasText(apiKeyId) == false) {
|
||||
logger.trace("none of the parameters [api key id, api key name, username, realm name] were specified for retrieval");
|
||||
listener.onFailure(new IllegalArgumentException("One of [api key id, api key name, username, realm name] must be specified"));
|
||||
} else {
|
||||
findApiKeysForUserRealmApiKeyIdAndNameCombination(realmName, username, apiKeyName, apiKeyId, false, false,
|
||||
ActionListener.wrap(apiKeyInfos -> {
|
||||
if (apiKeyInfos.isEmpty()) {
|
||||
@ -904,7 +899,6 @@ public class ApiKeyService {
|
||||
}
|
||||
}, listener::onFailure));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns realm name for the authenticated user.
|
||||
|
@ -7,7 +7,6 @@
|
||||
package org.elasticsearch.xpack.security.authc;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.action.DocWriteResponse;
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
|
||||
@ -50,6 +49,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME;
|
||||
import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS;
|
||||
@ -90,6 +90,8 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
@Override
|
||||
public String configRoles() {
|
||||
return super.configRoles() + "\n" +
|
||||
"no_api_key_role:\n" +
|
||||
" cluster: [\"manage_token\"]\n" +
|
||||
"manage_api_key_role:\n" +
|
||||
" cluster: [\"manage_api_key\"]\n" +
|
||||
"manage_own_api_key_role:\n" +
|
||||
@ -101,6 +103,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
final String usersPasswdHashed = new String(
|
||||
getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
|
||||
return super.configUsers() +
|
||||
"user_with_no_api_key_role:" + usersPasswdHashed + "\n" +
|
||||
"user_with_manage_api_key_role:" + usersPasswdHashed + "\n" +
|
||||
"user_with_manage_own_api_key_role:" + usersPasswdHashed + "\n";
|
||||
}
|
||||
@ -108,6 +111,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
@Override
|
||||
public String configUsersRoles() {
|
||||
return super.configUsersRoles() +
|
||||
"no_api_key_role:user_with_no_api_key_role\n" +
|
||||
"manage_api_key_role:user_with_manage_api_key_role\n" +
|
||||
"manage_own_api_key_role:user_with_manage_own_api_key_role\n";
|
||||
}
|
||||
@ -560,6 +564,51 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
response, userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null);
|
||||
}
|
||||
|
||||
public void testGetAllApiKeys() throws InterruptedException, ExecutionException {
|
||||
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
|
||||
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
|
||||
int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7);
|
||||
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
|
||||
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
|
||||
noOfApiKeysForUserWithManageApiKeyRole, null, "monitor");
|
||||
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role",
|
||||
noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor");
|
||||
|
||||
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
|
||||
.basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
|
||||
final SecurityClient securityClient = new SecurityClient(client);
|
||||
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
||||
securityClient.getApiKey(new GetApiKeyRequest(), listener);
|
||||
GetApiKeyResponse response = listener.get();
|
||||
int totalApiKeys = noOfSuperuserApiKeys + noOfApiKeysForUserWithManageApiKeyRole + noOfApiKeysForUserWithManageOwnApiKeyRole;
|
||||
List<CreateApiKeyResponse> allApiKeys = new ArrayList<>();
|
||||
Stream.of(defaultUserCreatedKeys, userWithManageApiKeyRoleApiKeys, userWithManageOwnApiKeyRoleApiKeys).forEach(
|
||||
allApiKeys::addAll);
|
||||
verifyGetResponse(new String[]{SecuritySettingsSource.TEST_SUPERUSER, "user_with_manage_api_key_role",
|
||||
"user_with_manage_own_api_key_role"}, totalApiKeys, allApiKeys, response,
|
||||
allApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null);
|
||||
}
|
||||
|
||||
public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() throws InterruptedException, ExecutionException {
|
||||
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
|
||||
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
|
||||
int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7);
|
||||
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
|
||||
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
|
||||
noOfApiKeysForUserWithManageApiKeyRole, null, "monitor");
|
||||
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role",
|
||||
noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor");
|
||||
|
||||
final String withUser = randomFrom("user_with_manage_own_api_key_role", "user_with_no_api_key_role");
|
||||
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
|
||||
.basicAuthHeaderValue(withUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
|
||||
final SecurityClient securityClient = new SecurityClient(client);
|
||||
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
||||
securityClient.getApiKey(new GetApiKeyRequest(), listener);
|
||||
ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> listener.actionGet());
|
||||
assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", withUser);
|
||||
}
|
||||
|
||||
public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException {
|
||||
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
|
||||
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
|
||||
@ -646,6 +695,11 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
|
||||
private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
|
||||
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
|
||||
verifyGetResponse(new String[]{user}, expectedNumberOfApiKeys, responses, response, validApiKeyIds, invalidatedApiKeyIds);
|
||||
}
|
||||
|
||||
private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
|
||||
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
|
||||
assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys));
|
||||
List<String> expectedIds = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getId())
|
||||
.collect(Collectors.toList());
|
||||
@ -658,7 +712,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
.collect(Collectors.toList());
|
||||
assertThat(actualNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY)));
|
||||
Set<String> expectedUsernames = (validApiKeyIds.isEmpty()) ? Collections.emptySet()
|
||||
: Collections.singleton(user);
|
||||
: Sets.newHashSet(user);
|
||||
Set<String> actualUsernames = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false)
|
||||
.map(o -> o.getUsername()).collect(Collectors.toSet());
|
||||
assertThat(actualUsernames, containsInAnyOrder(expectedUsernames.toArray(Strings.EMPTY_ARRAY)));
|
||||
@ -695,4 +749,9 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
||||
assertThat(ese.getMessage(),
|
||||
is("action [" + action + "] is unauthorized for API key id [" + apiKeyId + "] of user [" + userName + "]"));
|
||||
}
|
||||
|
||||
private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName) {
|
||||
assertThat(ese.getMessage(),
|
||||
is("action [" + action + "] is unauthorized for user [" + userName + "]"));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user