Skip to content

Commit 7c862fe

Browse files
authored
Add support to retrieve all API keys if user has privilege (#47274) (#47641)
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
1 parent f93bb9d commit 7c862fe

File tree

9 files changed

+120
-34
lines changed

9 files changed

+120
-34
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ public final class GetApiKeyRequest implements Validatable, ToXContentObject {
3838
private final String name;
3939
private final boolean ownedByAuthenticatedUser;
4040

41+
private GetApiKeyRequest() {
42+
this(null, null, null, null, false);
43+
}
44+
4145
// pkg scope for testing
4246
GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
4347
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
44-
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
45-
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
46-
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
47-
}
4848
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
4949
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
5050
throwValidationError(
@@ -147,6 +147,13 @@ public static GetApiKeyRequest forOwnedApiKeys() {
147147
return new GetApiKeyRequest(null, null, null, null, true);
148148
}
149149

150+
/**
151+
* Creates get api key request to retrieve api key information for all api keys if the authenticated user is authorized to do so.
152+
*/
153+
public static GetApiKeyRequest forAllApiKeys() {
154+
return new GetApiKeyRequest();
155+
}
156+
150157
@Override
151158
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
152159
return builder;

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

+12
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,18 @@ public void testGetApiKey() throws Exception {
19831983
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
19841984
}
19851985

1986+
{
1987+
// tag::get-all-api-keys-request
1988+
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.forAllApiKeys();
1989+
// end::get-all-api-keys-request
1990+
1991+
GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
1992+
1993+
assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue()));
1994+
assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1));
1995+
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
1996+
}
1997+
19861998
{
19871999
// tag::get-user-realm-api-keys-request
19882000
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("default_file", "test_user");

client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyRequestTests.java

-2
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,13 @@ public void testRequestValidation() {
5252

5353
public void testRequestValidationFailureScenarios() throws IOException {
5454
String[][] inputs = new String[][] {
55-
{ randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), "false" },
5655
{ randomNullOrEmptyString(), "user", "api-kid", "api-kname", "false" },
5756
{ "realm", randomNullOrEmptyString(), "api-kid", "api-kname", "false" },
5857
{ "realm", "user", "api-kid", randomNullOrEmptyString(), "false" },
5958
{ randomNullOrEmptyString(), randomNullOrEmptyString(), "api-kid", "api-kname", "false" },
6059
{ "realm", randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(), "true"},
6160
{ randomNullOrEmptyString(), "user", randomNullOrEmptyString(), randomNullOrEmptyString(), "true"} };
6261
String[] expectedErrorMessages = new String[] {
63-
"One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false",
6462
"username or realm name must not be specified when the api key id or api key name is specified",
6563
"username or realm name must not be specified when the api key id or api key name is specified",
6664
"username or realm name must not be specified when the api key id or api key name is specified",

docs/java-rest/high-level/security/get-api-key.asciidoc

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ The +{request}+ supports retrieving API key information for
2323

2424
. A specific key or all API keys owned by the current authenticated user
2525

26+
. All API keys if the user is authorized to do so
27+
2628
===== Retrieve a specific API key by its id
2729
["source","java",subs="attributes,callouts,macros"]
2830
--------------------------------------------------
@@ -59,6 +61,12 @@ include-tagged::{doc-tests-file}[get-user-realm-api-keys-request]
5961
include-tagged::{doc-tests-file}[get-api-keys-owned-by-authenticated-user-request]
6062
--------------------------------------------------
6163

64+
===== Retrieve all API keys if the user is authorized to do so
65+
["source","java",subs="attributes,callouts,macros"]
66+
--------------------------------------------------
67+
include-tagged::{doc-tests-file}[get-all-api-keys-request]
68+
--------------------------------------------------
69+
6270
include::../execution.asciidoc[]
6371

6472
[id="{upid}-{api}-response"]

x-pack/docs/en/rest-api/security/get-api-keys.asciidoc

+11-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ by the currently authenticated user. Defaults to false.
5151
The 'realm_name' or 'username' parameters cannot be specified when this
5252
parameter is set to 'true' as they are assumed to be the currently authenticated ones.
5353

54-
NOTE: At least one of "id", "name", "username" and "realm_name" must be specified
55-
if "owner" is "false" (default).
54+
NOTE: When none of the parameters "id", "name", "username" and "realm_name"
55+
are specified, and the "owner" is set to false then it will retrieve all API
56+
keys if the user is authorized. If the user is not authorized to retrieve other user's
57+
API keys, then an error will be returned.
5658

5759
[[security-api-get-api-key-example]]
5860
==== {api-examples-title}
@@ -123,6 +125,13 @@ GET /_security/api_key?owner=true
123125
--------------------------------------------------
124126
// TEST[continued]
125127

128+
The following example retrieves all API keys if the user is authorized to do so:
129+
[source,console]
130+
--------------------------------------------------
131+
GET /_security/api_key
132+
--------------------------------------------------
133+
// TEST[continued]
134+
126135
Following creates an API key
127136

128137
[source,console]

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,16 @@ public static GetApiKeyRequest forOwnedApiKeys() {
133133
return new GetApiKeyRequest(null, null, null, null, true);
134134
}
135135

136+
/**
137+
* Creates get api key request to retrieve api key information for all api keys if the authenticated user is authorized to do so.
138+
*/
139+
public static GetApiKeyRequest forAllApiKeys() {
140+
return new GetApiKeyRequest();
141+
}
142+
136143
@Override
137144
public ActionRequestValidationException validate() {
138145
ActionRequestValidationException validationException = null;
139-
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
140-
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
141-
validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified if " +
142-
"[owner] flag is false", validationException);
143-
}
144146
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
145147
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
146148
validationException = addValidationError(

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java

-3
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ public void writeTo(StreamOutput out) throws IOException {
7676
}
7777

7878
String[][] inputs = new String[][]{
79-
{randomNullOrEmptyString(), randomNullOrEmptyString(), randomNullOrEmptyString(),
80-
randomNullOrEmptyString(), "false"},
8179
{randomNullOrEmptyString(), "user", "api-kid", "api-kname", "false"},
8280
{"realm", randomNullOrEmptyString(), "api-kid", "api-kname", "false"},
8381
{"realm", "user", "api-kid", randomNullOrEmptyString(), "false"},
@@ -86,7 +84,6 @@ public void writeTo(StreamOutput out) throws IOException {
8684
{randomNullOrEmptyString(), "user", randomNullOrEmptyString(), randomNullOrEmptyString(), "true"}
8785
};
8886
String[][] expectedErrorMessages = new String[][]{
89-
{"One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false"},
9087
{"username or realm name must not be specified when the api key id or api key name is specified",
9188
"only one of [api key id, api key name] can be specified"},
9289
{"username or realm name must not be specified when the api key id or api key name is specified",

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java

+10-16
Original file line numberDiff line numberDiff line change
@@ -888,22 +888,16 @@ private void maybeStartApiKeyRemover() {
888888
public void getApiKeys(String realmName, String username, String apiKeyName, String apiKeyId,
889889
ActionListener<GetApiKeyResponse> listener) {
890890
ensureEnabled();
891-
if (Strings.hasText(realmName) == false && Strings.hasText(username) == false && Strings.hasText(apiKeyName) == false
892-
&& Strings.hasText(apiKeyId) == false) {
893-
logger.trace("none of the parameters [api key id, api key name, username, realm name] were specified for retrieval");
894-
listener.onFailure(new IllegalArgumentException("One of [api key id, api key name, username, realm name] must be specified"));
895-
} else {
896-
findApiKeysForUserRealmApiKeyIdAndNameCombination(realmName, username, apiKeyName, apiKeyId, false, false,
897-
ActionListener.wrap(apiKeyInfos -> {
898-
if (apiKeyInfos.isEmpty()) {
899-
logger.debug("No active api keys found for realm [{}], user [{}], api key name [{}] and api key id [{}]",
900-
realmName, username, apiKeyName, apiKeyId);
901-
listener.onResponse(GetApiKeyResponse.emptyResponse());
902-
} else {
903-
listener.onResponse(new GetApiKeyResponse(apiKeyInfos));
904-
}
905-
}, listener::onFailure));
906-
}
891+
findApiKeysForUserRealmApiKeyIdAndNameCombination(realmName, username, apiKeyName, apiKeyId, false, false,
892+
ActionListener.wrap(apiKeyInfos -> {
893+
if (apiKeyInfos.isEmpty()) {
894+
logger.debug("No active api keys found for realm [{}], user [{}], api key name [{}] and api key id [{}]",
895+
realmName, username, apiKeyName, apiKeyId);
896+
listener.onResponse(GetApiKeyResponse.emptyResponse());
897+
} else {
898+
listener.onResponse(new GetApiKeyResponse(apiKeyInfos));
899+
}
900+
}, listener::onFailure));
907901
}
908902

909903
/**

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java

+61-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
package org.elasticsearch.xpack.security.authc;
88

99
import com.google.common.collect.Sets;
10-
1110
import org.elasticsearch.ElasticsearchSecurityException;
1211
import org.elasticsearch.action.DocWriteResponse;
1312
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
@@ -50,6 +49,7 @@
5049
import java.util.concurrent.ExecutionException;
5150
import java.util.concurrent.TimeUnit;
5251
import java.util.stream.Collectors;
52+
import java.util.stream.Stream;
5353

5454
import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME;
5555
import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS;
@@ -90,6 +90,8 @@ public void wipeSecurityIndex() throws Exception {
9090
@Override
9191
public String configRoles() {
9292
return super.configRoles() + "\n" +
93+
"no_api_key_role:\n" +
94+
" cluster: [\"manage_token\"]\n" +
9395
"manage_api_key_role:\n" +
9496
" cluster: [\"manage_api_key\"]\n" +
9597
"manage_own_api_key_role:\n" +
@@ -101,13 +103,15 @@ public String configUsers() {
101103
final String usersPasswdHashed = new String(
102104
getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
103105
return super.configUsers() +
106+
"user_with_no_api_key_role:" + usersPasswdHashed + "\n" +
104107
"user_with_manage_api_key_role:" + usersPasswdHashed + "\n" +
105108
"user_with_manage_own_api_key_role:" + usersPasswdHashed + "\n";
106109
}
107110

108111
@Override
109112
public String configUsersRoles() {
110113
return super.configUsersRoles() +
114+
"no_api_key_role:user_with_no_api_key_role\n" +
111115
"manage_api_key_role:user_with_manage_api_key_role\n" +
112116
"manage_own_api_key_role:user_with_manage_own_api_key_role\n";
113117
}
@@ -560,6 +564,51 @@ public void testGetApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedEx
560564
response, userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null);
561565
}
562566

567+
public void testGetAllApiKeys() throws InterruptedException, ExecutionException {
568+
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
569+
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
570+
int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7);
571+
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
572+
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
573+
noOfApiKeysForUserWithManageApiKeyRole, null, "monitor");
574+
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role",
575+
noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor");
576+
577+
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
578+
.basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
579+
final SecurityClient securityClient = new SecurityClient(client);
580+
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
581+
securityClient.getApiKey(new GetApiKeyRequest(), listener);
582+
GetApiKeyResponse response = listener.get();
583+
int totalApiKeys = noOfSuperuserApiKeys + noOfApiKeysForUserWithManageApiKeyRole + noOfApiKeysForUserWithManageOwnApiKeyRole;
584+
List<CreateApiKeyResponse> allApiKeys = new ArrayList<>();
585+
Stream.of(defaultUserCreatedKeys, userWithManageApiKeyRoleApiKeys, userWithManageOwnApiKeyRoleApiKeys).forEach(
586+
allApiKeys::addAll);
587+
verifyGetResponse(new String[]{SecuritySettingsSource.TEST_SUPERUSER, "user_with_manage_api_key_role",
588+
"user_with_manage_own_api_key_role"}, totalApiKeys, allApiKeys, response,
589+
allApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null);
590+
}
591+
592+
public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() throws InterruptedException, ExecutionException {
593+
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
594+
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
595+
int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7);
596+
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
597+
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
598+
noOfApiKeysForUserWithManageApiKeyRole, null, "monitor");
599+
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role",
600+
noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor");
601+
602+
final String withUser = randomFrom("user_with_manage_own_api_key_role", "user_with_no_api_key_role");
603+
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
604+
.basicAuthHeaderValue(withUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
605+
final SecurityClient securityClient = new SecurityClient(client);
606+
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
607+
securityClient.getApiKey(new GetApiKeyRequest(), listener);
608+
ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> listener.actionGet());
609+
assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", withUser);
610+
}
611+
563612
public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException {
564613
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
565614
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
@@ -646,6 +695,11 @@ private void verifyGetResponse(int expectedNumberOfApiKeys, List<CreateApiKeyRes
646695

647696
private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
648697
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
698+
verifyGetResponse(new String[]{user}, expectedNumberOfApiKeys, responses, response, validApiKeyIds, invalidatedApiKeyIds);
699+
}
700+
701+
private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
702+
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
649703
assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys));
650704
List<String> expectedIds = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getId())
651705
.collect(Collectors.toList());
@@ -658,7 +712,7 @@ private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List<Cr
658712
.collect(Collectors.toList());
659713
assertThat(actualNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY)));
660714
Set<String> expectedUsernames = (validApiKeyIds.isEmpty()) ? Collections.emptySet()
661-
: Collections.singleton(user);
715+
: Sets.newHashSet(user);
662716
Set<String> actualUsernames = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false)
663717
.map(o -> o.getUsername()).collect(Collectors.toSet());
664718
assertThat(actualUsernames, containsInAnyOrder(expectedUsernames.toArray(Strings.EMPTY_ARRAY)));
@@ -695,4 +749,9 @@ private void assertErrorMessage(final ElasticsearchSecurityException ese, String
695749
assertThat(ese.getMessage(),
696750
is("action [" + action + "] is unauthorized for API key id [" + apiKeyId + "] of user [" + userName + "]"));
697751
}
752+
753+
private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName) {
754+
assertThat(ese.getMessage(),
755+
is("action [" + action + "] is unauthorized for user [" + userName + "]"));
756+
}
698757
}

0 commit comments

Comments
 (0)