Skip to content

Commit 4098db8

Browse files
committed
Fix owner user realm check for API key authentication (elastic#84325)
API Key can run-as since elastic#79809. There are places in the code where we assume API key cannot run-as. Most of them are corrected in elastic#81564. But there are still a few things got missed. This PR fixes the methods for checking owner user realm for API key. This means, when API Keys "running-as" (impersonating other users), we do not expose the authenticating key ID and name to the end-user such as the Authenticate API and the SetSecurityUseringest processor. Only the effective user is revealed, just like in the regular case of a realm user run as. For audit logging, the key's ID and name are not exposed either. But this is mainly because there are no existing fields suitable for these information. We do intend to add them later (elastic#84394) because auditing logging is to consumed by system admin instead of end-users. Note the resource sharing check (canAccessResourcesOf) also needs to be fixed, this will be handled by elastic#84277
1 parent 615c3cd commit 4098db8

File tree

7 files changed

+152
-40
lines changed

7 files changed

+152
-40
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
253253
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
254254
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
255255
builder.field(User.Fields.EMAIL.getPreferredName(), user.email());
256-
if (isAuthenticatedWithServiceAccount()) {
256+
if (isServiceAccount()) {
257257
final String tokenName = (String) getMetadata().get(ServiceAccountSettings.TOKEN_NAME_FIELD);
258258
assert tokenName != null : "token name cannot be null";
259259
final String tokenSource = (String) getMetadata().get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
@@ -279,7 +279,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
279279
}
280280
builder.endObject();
281281
builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT));
282-
if (isAuthenticatedWithApiKey()) {
282+
if (isApiKey()) {
283283
this.assertApiKeyMetadata();
284284
final String apiKeyId = (String) this.metadata.get(AuthenticationField.API_KEY_ID_KEY);
285285
final String apiKeyName = (String) this.metadata.get(AuthenticationField.API_KEY_NAME_KEY);

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@
88
package org.elasticsearch.xpack.core.security.authc;
99

1010
import org.elasticsearch.Version;
11+
import org.elasticsearch.common.bytes.BytesArray;
12+
import org.elasticsearch.common.bytes.BytesReference;
1113
import org.elasticsearch.common.settings.Settings;
14+
import org.elasticsearch.common.xcontent.XContentHelper;
15+
import org.elasticsearch.core.Nullable;
1216
import org.elasticsearch.test.ESTestCase;
1317
import org.elasticsearch.test.VersionUtils;
18+
import org.elasticsearch.xcontent.ToXContent;
19+
import org.elasticsearch.xcontent.XContentBuilder;
20+
import org.elasticsearch.xcontent.XContentType;
1421
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
1522
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
1623
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
@@ -24,12 +31,15 @@
2431
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
2532
import org.elasticsearch.xpack.core.security.user.XPackUser;
2633

34+
import java.io.IOException;
2735
import java.util.Arrays;
2836
import java.util.EnumSet;
2937
import java.util.HashMap;
3038
import java.util.Locale;
3139
import java.util.Map;
40+
import java.util.Objects;
3241
import java.util.Set;
42+
import java.util.function.Consumer;
3343
import java.util.stream.Collectors;
3444

3545
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME;
@@ -38,7 +48,10 @@
3848
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
3949
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME;
4050
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE;
51+
import static org.hamcrest.Matchers.hasEntry;
52+
import static org.hamcrest.Matchers.hasKey;
4153
import static org.hamcrest.Matchers.is;
54+
import static org.hamcrest.Matchers.not;
4255

4356
public class AuthenticationTests extends ESTestCase {
4457

@@ -168,6 +181,41 @@ public void testIsServiceAccount() {
168181
}
169182
}
170183

184+
public void testToXContentWithApiKey() throws IOException {
185+
final String apiKeyId = randomAlphaOfLength(20);
186+
final Authentication authentication1 = randomApiKeyAuthentication(randomUser(), apiKeyId);
187+
final String apiKeyName = (String) authentication1.getMetadata().get(AuthenticationField.API_KEY_NAME_KEY);
188+
runWithAuthenticationToXContent(
189+
authentication1,
190+
m -> assertThat(
191+
m,
192+
hasEntry("api_key", apiKeyName != null ? Map.of("id", apiKeyId, "name", apiKeyName) : Map.of("id", apiKeyId))
193+
)
194+
);
195+
196+
final Authentication authentication2 = toRunAs(authentication1, randomUser(), randomRealm());
197+
runWithAuthenticationToXContent(authentication2, m -> assertThat(m, not(hasKey("api_key"))));
198+
}
199+
200+
public void testToXContentWithServiceAccount() throws IOException {
201+
final Authentication authentication1 = randomServiceAccountAuthentication();
202+
final String tokenName = (String) authentication1.getMetadata().get(ServiceAccountSettings.TOKEN_NAME_FIELD);
203+
final String tokenType = ServiceAccountSettings.REALM_TYPE
204+
+ "_"
205+
+ authentication1.getMetadata().get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
206+
runWithAuthenticationToXContent(
207+
authentication1,
208+
m -> assertThat(m, hasEntry("token", Map.of("name", tokenName, "type", tokenType)))
209+
);
210+
}
211+
212+
private void runWithAuthenticationToXContent(Authentication authentication, Consumer<Map<String, Object>> consumer) throws IOException {
213+
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
214+
authentication.toXContent(builder, ToXContent.EMPTY_PARAMS);
215+
consumer.accept(XContentHelper.convertToMap(BytesReference.bytes(builder), false, XContentType.JSON).v2());
216+
}
217+
}
218+
171219
private void checkCanAccessResources(Authentication authentication0, Authentication authentication1) {
172220
if (authentication0.getAuthenticationType() == authentication1.getAuthenticationType()
173221
|| EnumSet.of(AuthenticationType.REALM, AuthenticationType.TOKEN)
@@ -243,6 +291,11 @@ public static Authentication randomApiKeyAuthentication(User user, String apiKey
243291
final HashMap<String, Object> metadata = new HashMap<>();
244292
metadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId);
245293
metadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLengthBetween(1, 16));
294+
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_NAME, AuthenticationField.API_KEY_CREATOR_REALM_NAME);
295+
metadata.put(AuthenticationField.API_KEY_CREATOR_REALM_TYPE, AuthenticationField.API_KEY_CREATOR_REALM_TYPE);
296+
metadata.put(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, new BytesArray("{}"));
297+
metadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, new BytesArray("""
298+
{"x":{"cluster":["all"],"indices":[{"names":["index*"],"privileges":["all"]}]}}"""));
246299
return new Authentication(
247300
user,
248301
apiKeyRealm,
@@ -304,6 +357,22 @@ public static Authentication toToken(Authentication authentication) {
304357
return newTokenAuthentication;
305358
}
306359

360+
public static Authentication toRunAs(Authentication authentication, User runAs, @Nullable RealmRef lookupRealmRef) {
361+
Objects.requireNonNull(runAs);
362+
assert false == runAs.isRunAs();
363+
assert false == authentication.getUser().isRunAs();
364+
assert AuthenticationType.REALM == authentication.getAuthenticationType()
365+
|| AuthenticationType.API_KEY == authentication.getAuthenticationType();
366+
return new Authentication(
367+
new User(runAs, authentication.getUser()),
368+
authentication.getAuthenticatedBy(),
369+
lookupRealmRef,
370+
authentication.getVersion(),
371+
authentication.getAuthenticationType(),
372+
authentication.getMetadata()
373+
);
374+
}
375+
307376
private boolean realmIsSingleton(RealmRef realmRef) {
308377
return Set.of(FileRealmSettings.TYPE, NativeRealmSettings.TYPE).contains(realmRef.getType());
309378
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,13 +1470,13 @@ private void setThreadContextField(ThreadContext threadContext, String threadCon
14701470
LogEntryBuilder withAuthentication(Authentication authentication) {
14711471
logEntry.with(PRINCIPAL_FIELD_NAME, authentication.getUser().principal());
14721472
logEntry.with(AUTHENTICATION_TYPE_FIELD_NAME, authentication.getAuthenticationType().toString());
1473-
if (authentication.isAuthenticatedWithApiKey()) {
1473+
if (authentication.isApiKey()) {
14741474
logEntry.with(API_KEY_ID_FIELD_NAME, (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY));
14751475
String apiKeyName = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_NAME_KEY);
14761476
if (apiKeyName != null) {
14771477
logEntry.with(API_KEY_NAME_FIELD_NAME, apiKeyName);
14781478
}
1479-
String creatorRealmName = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_CREATOR_REALM_NAME);
1479+
final String creatorRealmName = ApiKeyService.getCreatorRealmName(authentication);
14801480
if (creatorRealmName != null) {
14811481
// can be null for API keys created before version 7.7
14821482
logEntry.with(PRINCIPAL_REALM_FIELD_NAME, creatorRealmName);
@@ -1485,11 +1485,15 @@ LogEntryBuilder withAuthentication(Authentication authentication) {
14851485
if (authentication.getUser().isRunAs()) {
14861486
logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getLookedUpBy().getName())
14871487
.with(PRINCIPAL_RUN_BY_FIELD_NAME, authentication.getUser().authenticatedUser().principal())
1488+
// API key can run-as, when that happens, the following field will be _es_api_key,
1489+
// not the API key owner user's realm.
14881490
.with(PRINCIPAL_RUN_BY_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName());
1491+
// TODO: API key can run-as which means we could use extra fields (#84394)
14891492
} else {
14901493
logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName());
14911494
}
14921495
}
1496+
// TODO: service token info is logged in a separate authentication field (#84394)
14931497
if (authentication.isAuthenticatedWithServiceAccount()) {
14941498
logEntry.with(SERVICE_TOKEN_NAME_FIELD_NAME, (String) authentication.getMetadata().get(TOKEN_NAME_FIELD))
14951499
.with(

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,31 +1335,29 @@ AtomicLong getLastEvictionCheckedAt() {
13351335
}
13361336

13371337
/**
1338-
* Returns realm name for the authenticated user.
1339-
* If the user is authenticated by realm type {@value AuthenticationField#API_KEY_REALM_TYPE}
1340-
* then it will return the realm name of user who created this API key.
1338+
* Returns realm name of the owner user of an API key if the effective user is an API Key.
1339+
* If the effective user is not an API key, it just returns the source realm name.
13411340
*
13421341
* @param authentication {@link Authentication}
13431342
* @return realm name
13441343
*/
13451344
public static String getCreatorRealmName(final Authentication authentication) {
1346-
if (authentication.isAuthenticatedWithApiKey()) {
1345+
if (authentication.isApiKey()) {
13471346
return (String) authentication.getMetadata().get(AuthenticationField.API_KEY_CREATOR_REALM_NAME);
13481347
} else {
13491348
return authentication.getSourceRealm().getName();
13501349
}
13511350
}
13521351

13531352
/**
1354-
* Returns realm type for the authenticated user.
1355-
* If the user is authenticated by realm type {@value AuthenticationField#API_KEY_REALM_TYPE}
1356-
* then it will return the realm name of user who created this API key.
1353+
* Returns realm type of the owner user of an API key if the effective user is an API Key.
1354+
* If the effective user is not an API key, it just returns the source realm type.
13571355
*
13581356
* @param authentication {@link Authentication}
13591357
* @return realm type
13601358
*/
13611359
public static String getCreatorRealmType(final Authentication authentication) {
1362-
if (authentication.isAuthenticatedWithApiKey()) {
1360+
if (authentication.isApiKey()) {
13631361
return (String) authentication.getMetadata().get(AuthenticationField.API_KEY_CREATOR_REALM_TYPE);
13641362
} else {
13651363
return authentication.getSourceRealm().getType();

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
140140
}
141141
break;
142142
case API_KEY:
143-
if (authentication.isAuthenticatedWithApiKey()) {
143+
if (authentication.isApiKey()) {
144144
final String apiKey = "api_key";
145145
final Object existingApiKeyField = userObject.get(apiKey);
146146
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)