Skip to content

Commit d4a22de

Browse files
authored
Fix isApiKey test and apply it consistently (#84396) (#84404)
Creating tokens using API keys is not properly supported till #80926. Previously the created token always has no previlege. Now the token has the same privilege as the API key itself (similar to user created tokens). Authenticating using the token is considered equivalent to the API key itself. Therefore the "isApiKey" check needs to be updated to cater for both authentications of API key itself and the token created by the API key. This PR updates the isApiKey check and apply it consistently to ensure the behaviour is consistent between an API key and a token created by it. The only exception is for supporting run-as. API key itself can run-as another user. But a token created by the API key cannot perform run-as (#84336) similar to how user/token works.
1 parent 4505442 commit d4a22de

File tree

11 files changed

+153
-24
lines changed

11 files changed

+153
-24
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public void executeAfterRewritingAuthentication(Consumer<StoredContext> consumer
187187
private Map<String, Object> rewriteMetadataForApiKeyRoleDescriptors(Version streamVersion, Authentication authentication) {
188188
Map<String, Object> metadata = authentication.getMetadata();
189189
// If authentication type is API key, regardless whether it has run-as, the metadata must contain API key role descriptors
190-
if (authentication.isAuthenticatedWithApiKey()) {
190+
if (authentication.isAuthenticatedAsApiKey()) {
191191
if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
192192
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
193193
metadata = new HashMap<>(metadata);

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,34 @@ public boolean isAuthenticatedWithServiceAccount() {
121121
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType());
122122
}
123123

124-
public boolean isAuthenticatedWithApiKey() {
125-
return AuthenticationType.API_KEY.equals(getAuthenticationType());
124+
/**
125+
* Whether the authenticating user is an API key, including a simple API key or a token created by an API key.
126+
* @return
127+
*/
128+
public boolean isAuthenticatedAsApiKey() {
129+
final boolean result = AuthenticationField.API_KEY_REALM_TYPE.equals(getAuthenticatedBy().getType());
130+
assert false == result || AuthenticationField.API_KEY_REALM_NAME.equals(getAuthenticatedBy().getName());
131+
return result;
126132
}
127133

128134
/**
129135
* Authenticate with a service account and no run-as
130136
*/
131137
public boolean isServiceAccount() {
132-
return isAuthenticatedWithServiceAccount() && false == getUser().isRunAs();
138+
final boolean result = ServiceAccountSettings.REALM_TYPE.equals(getSourceRealm().getType());
139+
assert false == result || ServiceAccountSettings.REALM_NAME.equals(getSourceRealm().getName())
140+
: "service account realm name mismatch";
141+
return result;
133142
}
134143

135144
/**
136-
* Authenticated with an API key and no run-as
145+
* Whether the effective user is an API key, this including a simple API key authentication
146+
* or a token created by the API key.
137147
*/
138148
public boolean isApiKey() {
139-
return isAuthenticatedWithApiKey() && false == getUser().isRunAs();
149+
final boolean result = AuthenticationField.API_KEY_REALM_TYPE.equals(getSourceRealm().getType());
150+
assert false == result || AuthenticationField.API_KEY_REALM_NAME.equals(getSourceRealm().getName()) : "api key realm name mismatch";
151+
return result;
140152
}
141153

142154
/**
@@ -292,7 +304,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
292304
}
293305

294306
private void assertApiKeyMetadata() {
295-
assert (false == isAuthenticatedWithApiKey()) || (this.metadata.get(AuthenticationField.API_KEY_ID_KEY) != null)
307+
assert (false == isAuthenticatedAsApiKey()) || (this.metadata.get(AuthenticationField.API_KEY_ID_KEY) != null)
296308
: "API KEY authentication requires metadata to contain API KEY id, and the value must be non-null.";
297309
}
298310

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ private Authentication createMockAuthentication(
187187
when(authentication.getSourceRealm()).thenReturn(authenticatedBy);
188188
when(authentication.getAuthenticationType()).thenReturn(authenticationType);
189189
when(authenticatedBy.getName()).thenReturn(realmName);
190+
when(authenticatedBy.getType()).thenReturn(realmName);
190191
when(authentication.getMetadata()).thenReturn(metadata);
191-
when(authentication.isAuthenticatedWithApiKey()).thenCallRealMethod();
192+
when(authentication.isAuthenticatedAsApiKey()).thenCallRealMethod();
192193
when(authentication.isApiKey()).thenCallRealMethod();
193194
return authentication;
194195
}

x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnActionTests.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.elasticsearch.xpack.idp.action;
88

9+
import org.elasticsearch.Version;
910
import org.elasticsearch.action.ActionListener;
1011
import org.elasticsearch.action.support.ActionFilters;
1112
import org.elasticsearch.action.support.PlainActionFuture;
@@ -19,6 +20,7 @@
1920
import org.elasticsearch.transport.TransportService;
2021
import org.elasticsearch.xpack.core.security.SecurityContext;
2122
import org.elasticsearch.xpack.core.security.authc.Authentication;
23+
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
2224
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
2325
import org.elasticsearch.xpack.core.security.user.User;
2426
import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges;
@@ -40,6 +42,7 @@
4042
import java.time.Duration;
4143
import java.util.Collections;
4244
import java.util.HashMap;
45+
import java.util.Map;
4346
import java.util.Set;
4447

4548
import static org.hamcrest.CoreMatchers.containsString;
@@ -162,7 +165,10 @@ private TransportSamlInitiateSingleSignOnAction setupTransportAction(boolean wit
162165
true
163166
),
164167
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name"),
165-
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name")
168+
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name"),
169+
Version.CURRENT,
170+
Authentication.AuthenticationType.API_KEY,
171+
Map.of(AuthenticationField.API_KEY_ID_KEY, randomAlphaOfLength(20))
166172
)
167173
).writeToContext(threadContext);
168174
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
5353
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
5454
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse;
55+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
56+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequestBuilder;
57+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
5558
import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
5659
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
5760
import org.elasticsearch.xpack.core.security.action.user.PutUserResponse;
@@ -109,6 +112,7 @@ public Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
109112
return Settings.builder()
110113
.put(super.nodeSettings(nodeOrdinal, otherSettings))
111114
.put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true)
115+
.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true)
112116
.put(ApiKeyService.DELETE_INTERVAL.getKey(), TimeValue.timeValueMillis(DELETE_INTERVAL_MILLIS))
113117
.put(ApiKeyService.DELETE_TIMEOUT.getKey(), TimeValue.timeValueSeconds(5L))
114118
.put("xpack.security.crypto.thread_pool.queue_size", CRYPTO_THREAD_POOL_QUEUE_SIZE)
@@ -1111,7 +1115,9 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException {
11111115
Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
11121116
);
11131117
final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("key-1")
1114-
.setRoleDescriptors(Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key" }, null, null)))
1118+
.setRoleDescriptors(
1119+
Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key", "manage_token" }, null, null))
1120+
)
11151121
.setMetadata(ApiKeyTests.randomMetadata())
11161122
.get();
11171123

@@ -1122,7 +1128,17 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException {
11221128
// use the first ApiKey for authorized action
11231129
final String base64ApiKeyKeyValue = Base64.getEncoder()
11241130
.encodeToString((response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8));
1125-
final Client clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));
1131+
1132+
final Client clientKey1;
1133+
if (randomBoolean()) {
1134+
clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));
1135+
} else {
1136+
final CreateTokenResponse createTokenResponse = new CreateTokenRequestBuilder(
1137+
client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)),
1138+
CreateTokenAction.INSTANCE
1139+
).setGrantType("client_credentials").get();
1140+
clientKey1 = client().filterWithHeader(Map.of("Authorization", "Bearer " + createTokenResponse.getTokenString()));
1141+
}
11261142

11271143
final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty";
11281144

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/RunAsIntegTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public void testRunAsUsingApiKey() throws IOException {
157157
createApiKeyResponse.getEntity().getContent()
158158
);
159159

160-
final boolean runAsTestUser = false;
160+
final boolean runAsTestUser = randomBoolean();
161161

162162
final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
163163
authenticateRequest.setOptions(
@@ -194,6 +194,32 @@ public void testRunAsUsingApiKey() throws IOException {
194194
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
195195
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
196196
}
197+
198+
// Run-as ignored if using a token created by the API key
199+
final Request createTokenRequest = new Request("POST", "/_security/oauth2/token");
200+
createTokenRequest.setOptions(
201+
createTokenRequest.getOptions().toBuilder().addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
202+
);
203+
createTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}");
204+
final Response createTokenResponse = getRestClient().performRequest(createTokenRequest);
205+
final XContentTestUtils.JsonMapView createTokenJsonView = XContentTestUtils.createJsonMapView(
206+
createTokenResponse.getEntity().getContent()
207+
);
208+
209+
authenticateRequest.setOptions(
210+
RequestOptions.DEFAULT.toBuilder()
211+
.addHeader("Authorization", "Bearer " + createTokenJsonView.get("access_token"))
212+
.addHeader(
213+
AuthenticationServiceField.RUN_AS_USER_HEADER,
214+
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
215+
)
216+
);
217+
final Response authenticateResponse2 = getRestClient().performRequest(authenticateRequest);
218+
final XContentTestUtils.JsonMapView authenticateJsonView2 = XContentTestUtils.createJsonMapView(
219+
authenticateResponse2.getEntity().getContent()
220+
);
221+
// run-as header is ignored, the user is still the run_as_user
222+
assertThat(authenticateJsonView2.get("username"), equalTo(RUN_AS_USER));
197223
}
198224

199225
public void testRunAsIgnoredForOAuthToken() throws IOException {

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.action.get.GetResponse;
1616
import org.elasticsearch.action.main.MainAction;
1717
import org.elasticsearch.action.main.MainRequest;
18+
import org.elasticsearch.client.internal.Client;
1819
import org.elasticsearch.common.Strings;
1920
import org.elasticsearch.common.settings.SecureString;
2021
import org.elasticsearch.common.settings.Settings;
@@ -27,6 +28,9 @@
2728
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
2829
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest;
2930
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
31+
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
32+
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyRequest;
33+
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse;
3034
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
3135
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
3236
import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction;
@@ -35,6 +39,9 @@
3539
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction;
3640
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
3741
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse;
42+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
43+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequestBuilder;
44+
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
3845
import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
3946
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
4047
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
@@ -45,6 +52,7 @@
4552
import java.nio.charset.StandardCharsets;
4653
import java.time.Instant;
4754
import java.util.Base64;
55+
import java.util.Collections;
4856
import java.util.List;
4957
import java.util.Map;
5058

@@ -60,6 +68,7 @@ public class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
6068
protected Settings nodeSettings() {
6169
Settings.Builder builder = Settings.builder().put(super.nodeSettings());
6270
builder.put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true);
71+
builder.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true);
6372
return builder.build();
6473
}
6574

@@ -185,6 +194,50 @@ public void testServiceAccountApiKey() throws IOException {
185194
assertThat(roleDescriptor, equalTo(ServiceAccountService.getServiceAccounts().get("elastic/fleet-server").roleDescriptor()));
186195
}
187196

197+
public void testGetApiKeyWorksForTheApiKeyItself() {
198+
final String apiKeyName = randomAlphaOfLength(10);
199+
final CreateApiKeyResponse createApiKeyResponse = client().execute(
200+
CreateApiKeyAction.INSTANCE,
201+
new CreateApiKeyRequest(
202+
apiKeyName,
203+
List.of(new RoleDescriptor("x", new String[] { "manage_own_api_key", "manage_token" }, null, null, null, null, null, null)),
204+
null,
205+
null
206+
)
207+
).actionGet();
208+
209+
final String apiKeyId = createApiKeyResponse.getId();
210+
final String base64ApiKeyKeyValue = Base64.getEncoder()
211+
.encodeToString((apiKeyId + ":" + createApiKeyResponse.getKey().toString()).getBytes(StandardCharsets.UTF_8));
212+
213+
// Works for both the API key itself or the token created by it
214+
final Client clientKey1;
215+
if (randomBoolean()) {
216+
clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));
217+
} else {
218+
final CreateTokenResponse createTokenResponse = new CreateTokenRequestBuilder(
219+
client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)),
220+
CreateTokenAction.INSTANCE
221+
).setGrantType("client_credentials").get();
222+
clientKey1 = client().filterWithHeader(Map.of("Authorization", "Bearer " + createTokenResponse.getTokenString()));
223+
}
224+
225+
// Can get its own info
226+
final GetApiKeyResponse getApiKeyResponse = clientKey1.execute(
227+
GetApiKeyAction.INSTANCE,
228+
GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean())
229+
).actionGet();
230+
assertThat(getApiKeyResponse.getApiKeyInfos().length, equalTo(1));
231+
assertThat(getApiKeyResponse.getApiKeyInfos()[0].getId(), equalTo(apiKeyId));
232+
233+
// Cannot get any other keys
234+
final ElasticsearchSecurityException e = expectThrows(
235+
ElasticsearchSecurityException.class,
236+
() -> clientKey1.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forAllApiKeys()).actionGet()
237+
);
238+
assertThat(e.getMessage(), containsString("unauthorized for API key id [" + apiKeyId + "]"));
239+
}
240+
188241
private Map<String, Object> getApiKeyDocument(String apiKeyId) {
189242
final GetResponse getResponse = client().execute(GetAction.INSTANCE, new GetRequest(".security-7", apiKeyId)).actionGet();
190243
return getResponse.getSource();

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,10 +1371,12 @@ public static String getCreatorRealmType(final Authentication authentication) {
13711371
* @return A map for the metadata or an empty map if no metadata is found.
13721372
*/
13731373
public static Map<String, Object> getApiKeyMetadata(Authentication authentication) {
1374-
if (false == authentication.isAuthenticatedWithApiKey()) {
1374+
if (false == authentication.isAuthenticatedAsApiKey()) {
13751375
throw new IllegalArgumentException(
1376-
"authentication type must be [api_key], got ["
1377-
+ authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
1376+
"authentication realm must be ["
1377+
+ AuthenticationField.API_KEY_REALM_TYPE
1378+
+ "], got ["
1379+
+ authentication.getAuthenticatedBy().getType()
13781380
+ "]"
13791381
);
13801382
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ private ElasticsearchSecurityException denialException(
879879
userText = userText + " run as [" + authentication.getUser().principal() + "]";
880880
}
881881
// check for authentication by API key
882-
if (authentication.isAuthenticatedWithApiKey()) {
882+
if (authentication.isAuthenticatedAsApiKey()) {
883883
final String apiKeyId = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY);
884884
assert apiKeyId != null : "api key id must be present in the metadata";
885885
userText = "API key id [" + apiKeyId + "] of " + userText;

0 commit comments

Comments
 (0)