From d5bc9d221c32def6cc053d998cec23243361c6a5 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 10:21:04 +0200 Subject: [PATCH 01/18] [UIAM] Cloud API key authentication --- .../core/security/SecurityExtension.java | 5 ++ .../core/security/authc/Authentication.java | 29 ++++++++- .../security/authc/AuthenticationField.java | 3 + .../security/authc/CloudApiKeyService.java | 59 +++++++++++++++++ .../xpack/core/security/authc/Subject.java | 23 +++---- .../security/src/main/java/module-info.java | 2 +- .../xpack/security/Security.java | 17 ++++- .../security/authc/AuthenticationService.java | 5 +- .../security/authc/AuthenticatorChain.java | 9 ++- .../authc/CloudApiKeyAuthenticator.java | 54 ++++++++++++++++ .../authc/AuthenticationServiceTests.java | 63 +++++++++++-------- .../authc/AuthenticatorChainTests.java | 1 + .../support/SecondaryAuthenticatorTests.java | 3 +- 13 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index 9226a4148900d..b07c85cb80928 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -15,6 +15,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; +import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore; @@ -128,6 +129,10 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents return null; } + default CloudApiKeyService getCloudApiKeyService(SecurityComponents components) { + return null; + } + /** * Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism. * diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 561a94dd3cc05..4ebcfda9c7174 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -61,6 +61,7 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newAnonymousRealmRef; import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newApiKeyRealmRef; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newCloudApiKeyRealmRef; import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newCrossClusterAccessRealmRef; import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalAttachRealmRef; import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalFallbackRealmRef; @@ -71,6 +72,8 @@ import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_TYPE; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CLOUD_API_KEY_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CLOUD_API_KEY_REALM_TYPE; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_REALM_NAME; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_REALM_TYPE; @@ -891,11 +894,16 @@ private void checkConsistencyForInternalAuthenticationType() { private void checkConsistencyForApiKeyAuthenticationType() { final RealmRef authenticatingRealm = authenticatingSubject.getRealm(); - if (false == authenticatingRealm.isApiKeyRealm() && false == authenticatingRealm.isCrossClusterAccessRealm()) { + if (false == authenticatingRealm.isApiKeyRealm() + && false == authenticatingRealm.isCrossClusterAccessRealm() + && false == authenticatingRealm.isCloudApiKeyRealm()) { throw new IllegalArgumentException( Strings.format("API key authentication cannot have realm type [%s]", authenticatingRealm.type) ); } + if (authenticatingRealm.isCloudApiKeyRealm()) { + return; + } checkConsistencyForApiKeyAuthenticatingSubject("API key"); if (Subject.Type.CROSS_CLUSTER_ACCESS == authenticatingSubject.getType()) { if (authenticatingSubject.getMetadata().get(CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY) == null) { @@ -1183,6 +1191,10 @@ private boolean isAnonymousRealm() { return ANONYMOUS_REALM_NAME.equals(name) && ANONYMOUS_REALM_TYPE.equals(type); } + private boolean isCloudApiKeyRealm() { + return CLOUD_API_KEY_REALM_NAME.equals(name) && CLOUD_API_KEY_REALM_TYPE.equals(type); + } + private boolean isApiKeyRealm() { return API_KEY_REALM_NAME.equals(name) && API_KEY_REALM_TYPE.equals(type); } @@ -1212,6 +1224,11 @@ static RealmRef newServiceAccountRealmRef(String nodeName) { return new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName, null); } + static RealmRef newCloudApiKeyRealmRef(String nodeName) { + // no domain for cloud API key tokens + return new RealmRef(CLOUD_API_KEY_REALM_NAME, CLOUD_API_KEY_REALM_TYPE, nodeName, null); + } + static RealmRef newApiKeyRealmRef(String nodeName) { // no domain for API Key tokens return new RealmRef(API_KEY_REALM_NAME, API_KEY_REALM_TYPE, nodeName, null); @@ -1293,6 +1310,16 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef return authentication; } + public static Authentication newCloudApiKeyAuthentication(AuthenticationResult authResult, String nodeName) { + assert authResult.isAuthenticated() : "cloud API Key authn result must be successful"; + final User apiKeyUser = authResult.getValue(); + final Authentication.RealmRef authenticatedBy = newCloudApiKeyRealmRef(nodeName); + return new Authentication( + new Subject(apiKeyUser, authenticatedBy, TransportVersion.current(), authResult.getMetadata()), + AuthenticationType.API_KEY + ); + } + public static Authentication newApiKeyAuthentication(AuthenticationResult authResult, String nodeName) { assert authResult.isAuthenticated() : "API Key authn result must be successful"; final User apiKeyUser = authResult.getValue(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java index 37cf09bc4607a..7e6da3a82baa6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java @@ -13,6 +13,9 @@ public final class AuthenticationField { public static final String PRIVILEGE_CATEGORY_VALUE_OPERATOR = "operator"; public static final String PRIVILEGE_CATEGORY_VALUE_EMPTY = "__empty"; + public static final String CLOUD_API_KEY_REALM_NAME = "_cloud_api_key"; + public static final String CLOUD_API_KEY_REALM_TYPE = "_cloud_api_key"; + public static final String API_KEY_REALM_NAME = "_es_api_key"; public static final String API_KEY_REALM_TYPE = "_es_api_key"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java new file mode 100644 index 0000000000000..2dddb0779b171 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java @@ -0,0 +1,59 @@ +/* + * 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.core.security.authc; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.core.security.user.User; + +import java.io.Closeable; +import java.io.IOException; + +public interface CloudApiKeyService { + record CloudApiKey(SecureString authorizationHeader) implements AuthenticationToken, Closeable { + @Override + public String principal() { + return ""; + } + + @Override + public Object credentials() { + return authorizationHeader; + } + + @Override + public void clearCredentials() { + authorizationHeader.close(); + } + + @Override + public void close() throws IOException { + authorizationHeader.close(); + } + } + + @Nullable + CloudApiKey extractCloudApiKey(ThreadContext context); + + void authenticate(CloudApiKey cloudApiKey, ActionListener> listener); + + class Noop implements CloudApiKeyService { + @Override + public CloudApiKey extractCloudApiKey(ThreadContext context) { + return null; + } + + @Override + public void authenticate(CloudApiKey cloudApiKey, ActionListener> listener) { + assert false : "should never be called"; + listener.onResponse(AuthenticationResult.notHandled()); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java index 39173be73f191..ff67108796bff 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java @@ -47,6 +47,7 @@ public class Subject { public enum Type { USER, API_KEY, + CLOUD_API_KEY, SERVICE_ACCOUNT, CROSS_CLUSTER_ACCESS, } @@ -72,6 +73,9 @@ public Subject(User user, Authentication.RealmRef realm, TransportVersion versio } else if (AuthenticationField.API_KEY_REALM_TYPE.equals(realm.getType())) { assert AuthenticationField.API_KEY_REALM_NAME.equals(realm.getName()) : "api key realm name mismatch"; this.type = Type.API_KEY; + } else if (AuthenticationField.CLOUD_API_KEY_REALM_TYPE.equals(realm.getType())) { + assert AuthenticationField.CLOUD_API_KEY_REALM_NAME.equals(realm.getName()) : "cloud api key realm name mismatch"; + this.type = Type.CLOUD_API_KEY; } else if (ServiceAccountSettings.REALM_TYPE.equals(realm.getType())) { assert ServiceAccountSettings.REALM_NAME.equals(realm.getName()) : "service account realm name mismatch"; this.type = Type.SERVICE_ACCOUNT; @@ -105,19 +109,12 @@ public TransportVersion getTransportVersion() { } public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable AnonymousUser anonymousUser) { - switch (type) { - case USER: - return buildRoleReferencesForUser(anonymousUser); - case API_KEY: - return buildRoleReferencesForApiKey(); - case SERVICE_ACCOUNT: - return new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal())); - case CROSS_CLUSTER_ACCESS: - return buildRoleReferencesForCrossClusterAccess(); - default: - assert false : "unknown subject type: [" + type + "]"; - throw new IllegalStateException("unknown subject type: [" + type + "]"); - } + return switch (type) { + case CLOUD_API_KEY, USER -> buildRoleReferencesForUser(anonymousUser); + case API_KEY -> buildRoleReferencesForApiKey(); + case SERVICE_ACCOUNT -> new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal())); + case CROSS_CLUSTER_ACCESS -> buildRoleReferencesForCrossClusterAccess(); + }; } public boolean canAccessResourcesOf(Subject resourceCreatorSubject) { diff --git a/x-pack/plugin/security/src/main/java/module-info.java b/x-pack/plugin/security/src/main/java/module-info.java index 7af53479b0844..c749b316c2596 100644 --- a/x-pack/plugin/security/src/main/java/module-info.java +++ b/x-pack/plugin/security/src/main/java/module-info.java @@ -67,7 +67,7 @@ exports org.elasticsearch.xpack.security.action.settings to org.elasticsearch.server; exports org.elasticsearch.xpack.security.operator to org.elasticsearch.internal.operator, org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authz to org.elasticsearch.internal.security; - exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent; + exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent, org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authc.saml to org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.slowlog to org.elasticsearch.server; exports org.elasticsearch.xpack.security.authc.support to org.elasticsearch.internal.security; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 60b034294bcfc..f347717a6790c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -202,6 +202,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; +import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.InternalRealmsSettings; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -1101,6 +1102,19 @@ Collection createComponents( operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } + // TODO ensure internal extensions only + SetOnce cloudApiKeyService = new SetOnce<>(); + for (var extension : securityExtensions) { + CloudApiKeyService inner = extension.getCloudApiKeyService(extensionComponents); + if (inner != null) { + cloudApiKeyService.set(inner); + } + } + if (cloudApiKeyService.get() == null) { + cloudApiKeyService.set(new CloudApiKeyService.Noop()); + } + components.add(cloudApiKeyService.get()); + authcService.set( new AuthenticationService( settings, @@ -1113,7 +1127,8 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - telemetryProvider.getMeterRegistry() + telemetryProvider.getMeterRegistry(), + cloudApiKeyService.get() ) ); components.add(authcService.get()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index b9d3bd099a52b..c0e1c1f21be97 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -29,6 +29,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; @@ -92,7 +93,8 @@ public AuthenticationService( ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, OperatorPrivilegesService operatorPrivilegesService, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + CloudApiKeyService cloudApiKeyService ) { this.realms = realms; this.auditTrailService = auditTrailService; @@ -115,6 +117,7 @@ public AuthenticationService( new AuthenticationContextSerializer(), new ServiceAccountAuthenticator(serviceAccountService, nodeName, meterRegistry), new OAuth2TokenAuthenticator(tokenService, meterRegistry), + new CloudApiKeyAuthenticator(nodeName, cloudApiKeyService), new ApiKeyAuthenticator(apiKeyService, nodeName, meterRegistry), new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache, meterRegistry) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index d43909ab8e8d6..4b49898dc78ca 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -54,6 +54,7 @@ class AuthenticatorChain { AuthenticationContextSerializer authenticationSerializer, ServiceAccountAuthenticator serviceAccountAuthenticator, OAuth2TokenAuthenticator oAuth2TokenAuthenticator, + CloudApiKeyAuthenticator cloudApiKeyAuthenticator, ApiKeyAuthenticator apiKeyAuthenticator, RealmsAuthenticator realmsAuthenticator ) { @@ -64,7 +65,13 @@ class AuthenticatorChain { this.isAnonymousUserEnabled = AnonymousUser.isAnonymousEnabled(settings); this.authenticationSerializer = authenticationSerializer; this.realmsAuthenticator = realmsAuthenticator; - this.allAuthenticators = List.of(serviceAccountAuthenticator, oAuth2TokenAuthenticator, apiKeyAuthenticator, realmsAuthenticator); + this.allAuthenticators = List.of( + serviceAccountAuthenticator, + oAuth2TokenAuthenticator, + cloudApiKeyAuthenticator, + apiKeyAuthenticator, + realmsAuthenticator + ); } void authenticate(Authenticator.Context context, ActionListener originalListener) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java new file mode 100644 index 0000000000000..4e61ce9891a12 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java @@ -0,0 +1,54 @@ +/* + * 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.authc; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; + +public class CloudApiKeyAuthenticator implements Authenticator { + private final String nodeName; + private final CloudApiKeyService cloudApiKeyService; + + public CloudApiKeyAuthenticator(String nodeName, CloudApiKeyService cloudApiKeyService) { + this.nodeName = nodeName; + this.cloudApiKeyService = cloudApiKeyService; + } + + @Override + public String name() { + return "cloud api key"; + } + + @Override + public AuthenticationToken extractCredentials(Context context) { + return cloudApiKeyService.extractCloudApiKey(context.getThreadContext()); + } + + @Override + public void authenticate(Context context, ActionListener> listener) { + final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken(); + if (false == authenticationToken instanceof CloudApiKeyService.CloudApiKey) { + listener.onResponse(AuthenticationResult.notHandled()); + return; + } + + final CloudApiKeyService.CloudApiKey cloudApiKey = (CloudApiKeyService.CloudApiKey) authenticationToken; + + cloudApiKeyService.authenticate(cloudApiKey, listener.delegateFailureAndWrap((l, authenticationResult) -> { + if (authenticationResult.isAuthenticated()) { + l.onResponse(AuthenticationResult.success(Authentication.newCloudApiKeyAuthentication(authenticationResult, nodeName))); + // TODO handle termination etc. + } else { + l.onResponse(AuthenticationResult.notHandled()); + } + })); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 1b5bbd4de9a44..17ec18ca9eed4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -372,7 +372,8 @@ public void init() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); } @@ -665,7 +666,8 @@ public void testAuthenticateSmartRealmOrderingDisabled() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); @@ -791,7 +793,7 @@ public void testAuthenticateCached() throws Exception { public void testAuthenticateNonExistentRestRequestUserThrowsAuthenticationException() throws Exception { when(firstRealm.token(threadContext)).thenReturn( - new UsernamePasswordToken("idonotexist", new SecureString("passwd".toCharArray())) + new UsernamePasswordToken("idonotexist", new SecureString("passwd".toCharArray())) ); try { authenticateBlocking(restRequest, null); @@ -1049,7 +1051,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); boolean requestIdAlreadyPresent = randomBoolean(); SetOnce reqId = new SetOnce<>(); @@ -1100,7 +1103,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -1124,7 +1128,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); service.authenticate("_action", new InternalRequest(), InternalUsers.SYSTEM_USER, ActionListener.wrap(result -> { if (requestIdAlreadyPresent) { @@ -1187,7 +1192,8 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { @@ -1232,7 +1238,8 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; @@ -1297,7 +1304,8 @@ public void testAnonymousUserRest() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); RestRequest request = new FakeRestRequest(); @@ -1334,7 +1342,8 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); RestRequest request = new FakeRestRequest(); @@ -1366,7 +1375,8 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); InternalRequest message = new InternalRequest(); boolean requestIdAlreadyPresent = randomBoolean(); @@ -1402,7 +1412,8 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); InternalRequest message = new InternalRequest(); @@ -2069,23 +2080,23 @@ public void testExpiredToken() throws Exception { when(projectIndex.indexExists()).thenReturn(true); User user = new User("_username", "r1"); final Authentication expected = AuthenticationTestHelper.builder() - .user(user) - .realmRef(new RealmRef("realm", "custom", "node")) - .build(false); + .user(user) + .realmRef(new RealmRef("realm", "custom", "node")) + .build(false); PlainActionFuture tokenFuture = new PlainActionFuture<>(); Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { Authentication originatingAuth = AuthenticationTestHelper.builder() - .user(new User("creator")) - .realmRef(new RealmRef("test", "test", "test")) - .build(false); + .user(new User("creator")) + .realmRef(new RealmRef("test", "test", "test")) + .build(false); tokenService.createOAuth2Tokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - expected, - originatingAuth, - Collections.emptyMap(), - tokenFuture + newTokenBytes.v1(), + newTokenBytes.v2(), + expected, + originatingAuth, + Collections.emptyMap(), + tokenFuture ); } String token = tokenFuture.get().getAccessToken(); @@ -2104,8 +2115,8 @@ public void testExpiredToken() throws Exception { } threadContext.putHeader("Authorization", "Bearer " + token); ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, - () -> authenticateBlocking("_action", transportRequest, null, null) + ElasticsearchSecurityException.class, + () -> authenticateBlocking("_action", transportRequest, null, null) ); if (requestIdAlreadyPresent) { assertThat(expectAuditRequestId(threadContext), is(reqId.get())); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index 363593b83c9c4..72727eb77cca7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -103,6 +103,7 @@ public void init() { authenticationContextSerializer, serviceAccountAuthenticator, oAuth2TokenAuthenticator, + mock(), apiKeyAuthenticator, realmsAuthenticator ); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index f26cd59f7532c..3c908619a3d18 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -160,7 +160,8 @@ public void setupMocks() throws Exception { apiKeyService, serviceAccountService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE, - MeterRegistry.NOOP + MeterRegistry.NOOP, + mock() ); authenticator = new SecondaryAuthenticator(securityContext, authenticationService, auditTrail); } From c67364947ac39e54ca2e650565e357f65a3db89a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 12:29:35 +0200 Subject: [PATCH 02/18] Clean up --- .../core/src/main/java/module-info.java | 1 + .../core/security/SecurityExtension.java | 2 +- .../core/security/authc/Authentication.java | 6 ++ .../security/authc/CloudApiKeyService.java | 59 ------------------- .../security/authc/cloud/CloudApiKey.java | 50 ++++++++++++++++ .../authc/cloud/CloudApiKeyService.java | 33 +++++++++++ .../xpack/security/Security.java | 2 +- .../security/authc/AuthenticationService.java | 2 +- .../authc/CloudApiKeyAuthenticator.java | 27 +++++---- .../authc/AuthenticatorChainTests.java | 5 +- 10 files changed, 111 insertions(+), 76 deletions(-) delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java diff --git a/x-pack/plugin/core/src/main/java/module-info.java b/x-pack/plugin/core/src/main/java/module-info.java index c6f8376d63fa9..8eabfd37fcb3a 100644 --- a/x-pack/plugin/core/src/main/java/module-info.java +++ b/x-pack/plugin/core/src/main/java/module-info.java @@ -230,6 +230,7 @@ exports org.elasticsearch.xpack.core.watcher.trigger; exports org.elasticsearch.xpack.core.watcher.watch; exports org.elasticsearch.xpack.core.watcher; + exports org.elasticsearch.xpack.core.security.authc.cloud; provides org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber with diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index b07c85cb80928..f778e3df5e325 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -15,8 +15,8 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; -import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 4ebcfda9c7174..04c489d275d9b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -531,6 +531,10 @@ public boolean isApiKey() { return effectiveSubject.getType() == Subject.Type.API_KEY; } + public boolean isCloudApiKey() { + return effectiveSubject.getType() == Subject.Type.CLOUD_API_KEY; + } + public boolean isCrossClusterAccess() { return effectiveSubject.getType() == Subject.Type.CROSS_CLUSTER_ACCESS; } @@ -775,6 +779,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException { builder.field("api_key", Map.of("id", apiKeyId, "name", apiKeyName)); } } + // TODO cloud API key fields such as managed_by } public static Authentication getAuthenticationFromCrossClusterAccessMetadata(Authentication authentication) { @@ -902,6 +907,7 @@ private void checkConsistencyForApiKeyAuthenticationType() { ); } if (authenticatingRealm.isCloudApiKeyRealm()) { + // TODO consistency check for cloud API keys return; } checkConsistencyForApiKeyAuthenticatingSubject("API key"); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java deleted file mode 100644 index 2dddb0779b171..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/CloudApiKeyService.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.core.security.authc; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.core.security.user.User; - -import java.io.Closeable; -import java.io.IOException; - -public interface CloudApiKeyService { - record CloudApiKey(SecureString authorizationHeader) implements AuthenticationToken, Closeable { - @Override - public String principal() { - return ""; - } - - @Override - public Object credentials() { - return authorizationHeader; - } - - @Override - public void clearCredentials() { - authorizationHeader.close(); - } - - @Override - public void close() throws IOException { - authorizationHeader.close(); - } - } - - @Nullable - CloudApiKey extractCloudApiKey(ThreadContext context); - - void authenticate(CloudApiKey cloudApiKey, ActionListener> listener); - - class Noop implements CloudApiKeyService { - @Override - public CloudApiKey extractCloudApiKey(ThreadContext context) { - return null; - } - - @Override - public void authenticate(CloudApiKey cloudApiKey, ActionListener> listener) { - assert false : "should never be called"; - listener.onResponse(AuthenticationResult.notHandled()); - } - } -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java new file mode 100644 index 0000000000000..8d5dd1c09c0a4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java @@ -0,0 +1,50 @@ +/* + * 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.core.security.authc.cloud; + +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; + +import java.io.Closeable; +import java.io.IOException; + +public record CloudApiKey(SecureString cloudApiKeyCredentials) implements AuthenticationToken, Closeable { + private static final String CLOUD_API_KEY_CREDENTIAL_PREFIX = "essu_"; + + @Override + public String principal() { + return ""; + } + + @Override + public Object credentials() { + return cloudApiKeyCredentials; + } + + @Override + public void clearCredentials() { + cloudApiKeyCredentials.close(); + } + + @Override + public void close() throws IOException { + cloudApiKeyCredentials.close(); + } + + @Nullable + public static CloudApiKey fromApiKeyString(@Nullable SecureString apiKeyString) { + return apiKeyString != null && hasCloudApiKeyPrefix(apiKeyString) ? new CloudApiKey(apiKeyString) : null; + } + + private static boolean hasCloudApiKeyPrefix(SecureString apiKeyString) { + final String rawString = apiKeyString.toString(); + return rawString.length() > CLOUD_API_KEY_CREDENTIAL_PREFIX.length() + && rawString.regionMatches(true, 0, CLOUD_API_KEY_CREDENTIAL_PREFIX, 0, CLOUD_API_KEY_CREDENTIAL_PREFIX.length()); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java new file mode 100644 index 0000000000000..1b3271bf4e091 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java @@ -0,0 +1,33 @@ +/* + * 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.core.security.authc.cloud; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.core.security.authc.Authentication; + +public interface CloudApiKeyService { + @Nullable + CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString); + + void authenticate(CloudApiKey cloudApiKey, String nodeName, ActionListener listener); + + class Noop implements CloudApiKeyService { + @Override + public CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString) { + return null; + } + + @Override + public void authenticate(CloudApiKey cloudApiKey, String nodeName, ActionListener listener) { + assert false : "cloud API key authentication noop implementation should never be called"; + listener.onFailure(new IllegalStateException("cloud API key authentication is disabled")); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index f347717a6790c..cd103b4cbe7c6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -202,13 +202,13 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; -import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.InternalRealmsSettings; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.Subject; +import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index c0e1c1f21be97..1ebf50fc9d4e7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -29,8 +29,8 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.core.security.user.AnonymousUser; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java index 4e61ce9891a12..823dadf3b4979 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java @@ -11,7 +11,8 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.CloudApiKeyService; +import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKey; +import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; public class CloudApiKeyAuthenticator implements Authenticator { private final String nodeName; @@ -24,31 +25,31 @@ public CloudApiKeyAuthenticator(String nodeName, CloudApiKeyService cloudApiKeyS @Override public String name() { - return "cloud api key"; + return "cloud API key"; } @Override public AuthenticationToken extractCredentials(Context context) { - return cloudApiKeyService.extractCloudApiKey(context.getThreadContext()); + return cloudApiKeyService.parseAsCloudApiKey(context.getApiKeyString()); } @Override public void authenticate(Context context, ActionListener> listener) { final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken(); - if (false == authenticationToken instanceof CloudApiKeyService.CloudApiKey) { + if (false == authenticationToken instanceof CloudApiKey) { listener.onResponse(AuthenticationResult.notHandled()); return; } - final CloudApiKeyService.CloudApiKey cloudApiKey = (CloudApiKeyService.CloudApiKey) authenticationToken; + final CloudApiKey cloudApiKey = (CloudApiKey) authenticationToken; - cloudApiKeyService.authenticate(cloudApiKey, listener.delegateFailureAndWrap((l, authenticationResult) -> { - if (authenticationResult.isAuthenticated()) { - l.onResponse(AuthenticationResult.success(Authentication.newCloudApiKeyAuthentication(authenticationResult, nodeName))); - // TODO handle termination etc. - } else { - l.onResponse(AuthenticationResult.notHandled()); - } - })); + cloudApiKeyService.authenticate( + cloudApiKey, + nodeName, + ActionListener.wrap( + authentication -> listener.onResponse(AuthenticationResult.success(authentication)), + e -> listener.onFailure(context.getRequest().exceptionProcessingRequest(e, cloudApiKey)) + ) + ); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index 72727eb77cca7..88d584d93a0f3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -67,6 +67,7 @@ public class AuthenticatorChainTests extends ESTestCase { private ServiceAccountAuthenticator serviceAccountAuthenticator; private OAuth2TokenAuthenticator oAuth2TokenAuthenticator; private ApiKeyAuthenticator apiKeyAuthenticator; + private CloudApiKeyAuthenticator cloudApiKeyAuthenticator; private RealmsAuthenticator realmsAuthenticator; private Authentication authentication; private User fallbackUser; @@ -91,6 +92,7 @@ public void init() { oAuth2TokenAuthenticator = mock(OAuth2TokenAuthenticator.class); apiKeyAuthenticator = mock(ApiKeyAuthenticator.class); realmsAuthenticator = mock(RealmsAuthenticator.class); + cloudApiKeyAuthenticator = mock(CloudApiKeyAuthenticator.class); when(realms.getActiveRealms()).thenReturn(List.of(mock(Realm.class))); when(realms.getUnlicensedRealms()).thenReturn(List.of()); final User user = new User(randomAlphaOfLength(8)); @@ -103,7 +105,7 @@ public void init() { authenticationContextSerializer, serviceAccountAuthenticator, oAuth2TokenAuthenticator, - mock(), + cloudApiKeyAuthenticator, apiKeyAuthenticator, realmsAuthenticator ); @@ -322,6 +324,7 @@ public void testContextWithDirectWrongTokenFailsAuthn() { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); // 1. realms do not consume the token doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") From 3f6b6ffef2cf53609fdec404b014f82411eeed36 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 15:32:21 +0200 Subject: [PATCH 03/18] Nit --- .../xpack/core/security/authc/cloud/CloudApiKeyService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java index 1b3271bf4e091..182eb4f8acef6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java @@ -12,6 +12,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.authc.Authentication; +// TODO javadoc public interface CloudApiKeyService { @Nullable CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString); From 95c9a38d3d7b54a77690726c245afec3413fb66d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 16:30:53 +0200 Subject: [PATCH 04/18] Fix more tests --- .../xpack/security/authc/AuthenticatorChainTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index 88d584d93a0f3..df1ac79d487b8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -220,6 +220,7 @@ public void testAuthenticateWithApiKey() throws IOException { new ApiKeyCredentials(randomAlphaOfLength(20), apiKeySecret, randomFrom(ApiKey.Type.values())) ); doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @@ -262,6 +263,7 @@ public void testAuthenticateWithRealms() throws IOException { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") From d45fe0c49e4c236320e051f9f816a7b53e11ccf9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 18:22:24 +0200 Subject: [PATCH 05/18] Nit --- .../xpack/core/security/authc/cloud/CloudApiKeyService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java index 182eb4f8acef6..1b3271bf4e091 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java @@ -12,7 +12,6 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.authc.Authentication; -// TODO javadoc public interface CloudApiKeyService { @Nullable CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString); From 3be47f05e05d7d6e53f76e9f169f91f6554004c1 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 26 May 2025 20:47:15 +0200 Subject: [PATCH 06/18] Fix sig --- .../xpack/security/Security.java | 4 +- .../security/authc/AuthenticationService.java | 4 +- .../authc/AuthenticationServiceTests.java | 44 +++++++++---------- .../support/SecondaryAuthenticatorTests.java | 4 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index cd103b4cbe7c6..c34d492bdea6a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1127,8 +1127,8 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - telemetryProvider.getMeterRegistry(), - cloudApiKeyService.get() + cloudApiKeyService.get(), + telemetryProvider.getMeterRegistry() ) ); components.add(authcService.get()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 1ebf50fc9d4e7..487018c62c8f1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -93,8 +93,8 @@ public AuthenticationService( ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, OperatorPrivilegesService operatorPrivilegesService, - MeterRegistry meterRegistry, - CloudApiKeyService cloudApiKeyService + CloudApiKeyService cloudApiKeyService, + MeterRegistry meterRegistry ) { this.realms = realms; this.auditTrailService = auditTrailService; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 17ec18ca9eed4..d0a9ee9d78a43 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -372,8 +372,8 @@ public void init() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); } @@ -666,8 +666,8 @@ public void testAuthenticateSmartRealmOrderingDisabled() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); User user = new User("_username", "r1"); when(firstRealm.supports(token)).thenReturn(true); @@ -1051,8 +1051,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); boolean requestIdAlreadyPresent = randomBoolean(); SetOnce reqId = new SetOnce<>(); @@ -1103,8 +1103,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); threadContext2.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get()); @@ -1128,8 +1128,8 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); service.authenticate("_action", new InternalRequest(), InternalUsers.SYSTEM_USER, ActionListener.wrap(result -> { if (requestIdAlreadyPresent) { @@ -1192,8 +1192,8 @@ public void testWrongTokenDoesNotFallbackToAnonymous() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { @@ -1238,8 +1238,8 @@ public void testWrongApiKeyDoesNotFallbackToAnonymous() { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); doAnswer(invocationOnMock -> { final GetRequest request = (GetRequest) invocationOnMock.getArguments()[0]; @@ -1304,8 +1304,8 @@ public void testAnonymousUserRest() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); RestRequest request = new FakeRestRequest(); @@ -1342,8 +1342,8 @@ public void testAuthenticateRestRequestDisallowAnonymous() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); RestRequest request = new FakeRestRequest(); @@ -1375,8 +1375,8 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); InternalRequest message = new InternalRequest(); boolean requestIdAlreadyPresent = randomBoolean(); @@ -1412,8 +1412,8 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { apiKeyService, serviceAccountService, operatorPrivilegesService, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); InternalRequest message = new InternalRequest(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 3c908619a3d18..7e9c4b861cbc9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -160,8 +160,8 @@ public void setupMocks() throws Exception { apiKeyService, serviceAccountService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE, - MeterRegistry.NOOP, - mock() + mock(), + MeterRegistry.NOOP ); authenticator = new SecondaryAuthenticator(securityContext, authenticationService, auditTrail); } From 0b6bdff00319b7dc3767f2a17cac77555aaf3d5f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 27 May 2025 11:04:49 +0200 Subject: [PATCH 07/18] Fix not --- .../xpack/security/Security.java | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index c34d492bdea6a..10e08df39e8d5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1102,18 +1102,9 @@ Collection createComponents( operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } - // TODO ensure internal extensions only - SetOnce cloudApiKeyService = new SetOnce<>(); - for (var extension : securityExtensions) { - CloudApiKeyService inner = extension.getCloudApiKeyService(extensionComponents); - if (inner != null) { - cloudApiKeyService.set(inner); - } - } - if (cloudApiKeyService.get() == null) { - cloudApiKeyService.set(new CloudApiKeyService.Noop()); - } - components.add(cloudApiKeyService.get()); + final CloudApiKeyService cloudApiKeyService = createCloudApiKeyService(extensionComponents); + + components.add(cloudApiKeyService); authcService.set( new AuthenticationService( @@ -1127,7 +1118,7 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - cloudApiKeyService.get(), + cloudApiKeyService, telemetryProvider.getMeterRegistry() ) ); @@ -1263,6 +1254,37 @@ Collection createComponents( return components; } + private CloudApiKeyService createCloudApiKeyService(SecurityExtension.SecurityComponents extensionComponents) { + final SetOnce cloudApiKeyServiceSetOnce = new SetOnce<>(); + for (var extension : securityExtensions) { + final CloudApiKeyService cloudApiKeyService = extension.getCloudApiKeyService(extensionComponents); + if (cloudApiKeyService != null) { + if (false == isInternalExtension(extension)) { + throw new IllegalStateException( + "The [" + + extension.getClass().getName() + + "] extension tried to install a custom CloudApiKeyService. " + + "This functionality is not available to external extensions." + ); + } + boolean success = cloudApiKeyServiceSetOnce.trySet(cloudApiKeyService); + if (false == success) { + throw new IllegalStateException( + "The [" + + extension.getClass().getName() + + "] extension tried to install a custom CloudApiKeyService, but one has already been installed." + ); + } else { + logger.debug("CloudApiKeyService provided by extension [{}]", extension.extensionName()); + } + } + } + if (cloudApiKeyServiceSetOnce.get() == null) { + cloudApiKeyServiceSetOnce.set(new CloudApiKeyService.Noop()); + } + return cloudApiKeyServiceSetOnce.get(); + } + private ServiceAccountService createServiceAccountService( List components, CacheInvalidatorRegistry cacheInvalidatorRegistry, From c9747615a4729bbfd5cd467e5761b929533b9199 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 27 May 2025 11:59:06 +0200 Subject: [PATCH 08/18] Nit --- .../main/java/org/elasticsearch/xpack/security/Security.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 10e08df39e8d5..f5014cc6e8183 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1274,9 +1274,8 @@ private CloudApiKeyService createCloudApiKeyService(SecurityExtension.SecurityCo + extension.getClass().getName() + "] extension tried to install a custom CloudApiKeyService, but one has already been installed." ); - } else { - logger.debug("CloudApiKeyService provided by extension [{}]", extension.extensionName()); } + logger.debug("CloudApiKeyService provided by extension [{}]", extension.extensionName()); } } if (cloudApiKeyServiceSetOnce.get() == null) { From 6966cea17a9690401a95dca878b71b1ad549ac17 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 14:50:22 +0200 Subject: [PATCH 09/18] Authenticator --- .../core/src/main/java/module-info.java | 2 +- .../core/security/SecurityExtension.java | 4 +- .../apikey/CustomApiKeyAuthenticator.java | 43 +++++++++++++++ .../security/authc/cloud/CloudApiKey.java | 50 ----------------- .../authc/cloud/CloudApiKeyService.java | 33 ----------- .../xpack/security/Security.java | 30 +++++----- .../security/authc/AuthenticationService.java | 6 +- .../security/authc/AuthenticatorChain.java | 4 +- .../authc/CloudApiKeyAuthenticator.java | 55 ------------------- .../authc/PluggableApiKeyAuthenticator.java | 44 +++++++++++++++ .../authc/AuthenticatorChainTests.java | 12 ++-- 11 files changed, 116 insertions(+), 167 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java diff --git a/x-pack/plugin/core/src/main/java/module-info.java b/x-pack/plugin/core/src/main/java/module-info.java index 8eabfd37fcb3a..3d28eff88a24a 100644 --- a/x-pack/plugin/core/src/main/java/module-info.java +++ b/x-pack/plugin/core/src/main/java/module-info.java @@ -230,7 +230,7 @@ exports org.elasticsearch.xpack.core.watcher.trigger; exports org.elasticsearch.xpack.core.watcher.watch; exports org.elasticsearch.xpack.core.watcher; - exports org.elasticsearch.xpack.core.security.authc.cloud; + exports org.elasticsearch.xpack.core.security.authc.apikey; provides org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber with diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index f778e3df5e325..f41b19de95272 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -16,7 +16,7 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.Realm; -import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator; import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; @@ -129,7 +129,7 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents return null; } - default CloudApiKeyService getCloudApiKeyService(SecurityComponents components) { + default CustomApiKeyAuthenticator getCustomApiKeyAuthenticator(SecurityComponents components) { return null; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java new file mode 100644 index 0000000000000..c2f79df940cd0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java @@ -0,0 +1,43 @@ +/* + * 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.core.security.authc.apikey; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; + +public interface CustomApiKeyAuthenticator { + String name(); + + AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials); + + void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener> listener); + + class Noop implements CustomApiKeyAuthenticator { + @Override + public String name() { + return "noop"; + } + + @Override + public AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials) { + return null; + } + + @Override + public void authenticate( + @Nullable AuthenticationToken authenticationToken, + ActionListener> listener + ) { + listener.onResponse(AuthenticationResult.notHandled()); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java deleted file mode 100644 index 8d5dd1c09c0a4..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKey.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.core.security.authc.cloud; - -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; - -import java.io.Closeable; -import java.io.IOException; - -public record CloudApiKey(SecureString cloudApiKeyCredentials) implements AuthenticationToken, Closeable { - private static final String CLOUD_API_KEY_CREDENTIAL_PREFIX = "essu_"; - - @Override - public String principal() { - return ""; - } - - @Override - public Object credentials() { - return cloudApiKeyCredentials; - } - - @Override - public void clearCredentials() { - cloudApiKeyCredentials.close(); - } - - @Override - public void close() throws IOException { - cloudApiKeyCredentials.close(); - } - - @Nullable - public static CloudApiKey fromApiKeyString(@Nullable SecureString apiKeyString) { - return apiKeyString != null && hasCloudApiKeyPrefix(apiKeyString) ? new CloudApiKey(apiKeyString) : null; - } - - private static boolean hasCloudApiKeyPrefix(SecureString apiKeyString) { - final String rawString = apiKeyString.toString(); - return rawString.length() > CLOUD_API_KEY_CREDENTIAL_PREFIX.length() - && rawString.regionMatches(true, 0, CLOUD_API_KEY_CREDENTIAL_PREFIX, 0, CLOUD_API_KEY_CREDENTIAL_PREFIX.length()); - } -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java deleted file mode 100644 index 1b3271bf4e091..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/cloud/CloudApiKeyService.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.core.security.authc.cloud; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.core.security.authc.Authentication; - -public interface CloudApiKeyService { - @Nullable - CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString); - - void authenticate(CloudApiKey cloudApiKey, String nodeName, ActionListener listener); - - class Noop implements CloudApiKeyService { - @Override - public CloudApiKey parseAsCloudApiKey(@Nullable SecureString apiKeyString) { - return null; - } - - @Override - public void authenticate(CloudApiKey cloudApiKey, String nodeName, ActionListener listener) { - assert false : "cloud API key authentication noop implementation should never be called"; - listener.onFailure(new IllegalStateException("cloud API key authentication is disabled")); - } - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index f5014cc6e8183..5b9a96f67cae9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -208,7 +208,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.Subject; -import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator; import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; @@ -1102,9 +1102,9 @@ Collection createComponents( operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } - final CloudApiKeyService cloudApiKeyService = createCloudApiKeyService(extensionComponents); + final CustomApiKeyAuthenticator customApiKeyAuthenticator = createCustomApiKeyAuthenticator(extensionComponents); - components.add(cloudApiKeyService); + components.add(customApiKeyAuthenticator); authcService.set( new AuthenticationService( @@ -1118,7 +1118,7 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - cloudApiKeyService, + customApiKeyAuthenticator, telemetryProvider.getMeterRegistry() ) ); @@ -1254,34 +1254,34 @@ Collection createComponents( return components; } - private CloudApiKeyService createCloudApiKeyService(SecurityExtension.SecurityComponents extensionComponents) { - final SetOnce cloudApiKeyServiceSetOnce = new SetOnce<>(); + private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) { + final SetOnce customApiKeyAuthenticatorSetOnce = new SetOnce<>(); for (var extension : securityExtensions) { - final CloudApiKeyService cloudApiKeyService = extension.getCloudApiKeyService(extensionComponents); - if (cloudApiKeyService != null) { + final CustomApiKeyAuthenticator customApiKeyAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents); + if (customApiKeyAuthenticator != null) { if (false == isInternalExtension(extension)) { throw new IllegalStateException( "The [" + extension.getClass().getName() - + "] extension tried to install a custom CloudApiKeyService. " + + "] extension tried to install a custom CustomApiKeyAuthenticator. " + "This functionality is not available to external extensions." ); } - boolean success = cloudApiKeyServiceSetOnce.trySet(cloudApiKeyService); + boolean success = customApiKeyAuthenticatorSetOnce.trySet(customApiKeyAuthenticator); if (false == success) { throw new IllegalStateException( "The [" + extension.getClass().getName() - + "] extension tried to install a custom CloudApiKeyService, but one has already been installed." + + "] extension tried to install a custom CustomApiKeyAuthenticator, but one has already been installed." ); } - logger.debug("CloudApiKeyService provided by extension [{}]", extension.extensionName()); + logger.debug("CustomApiKeyAuthenticator provided by extension [{}]", extension.extensionName()); } } - if (cloudApiKeyServiceSetOnce.get() == null) { - cloudApiKeyServiceSetOnce.set(new CloudApiKeyService.Noop()); + if (customApiKeyAuthenticatorSetOnce.get() == null) { + customApiKeyAuthenticatorSetOnce.set(new CustomApiKeyAuthenticator.Noop()); } - return cloudApiKeyServiceSetOnce.get(); + return customApiKeyAuthenticatorSetOnce.get(); } private ServiceAccountService createServiceAccountService( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 487018c62c8f1..a9c513a605fe8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -30,7 +30,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; -import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.core.security.user.AnonymousUser; @@ -93,7 +93,7 @@ public AuthenticationService( ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, OperatorPrivilegesService operatorPrivilegesService, - CloudApiKeyService cloudApiKeyService, + CustomApiKeyAuthenticator customApiKeyAuthenticator, MeterRegistry meterRegistry ) { this.realms = realms; @@ -117,7 +117,7 @@ public AuthenticationService( new AuthenticationContextSerializer(), new ServiceAccountAuthenticator(serviceAccountService, nodeName, meterRegistry), new OAuth2TokenAuthenticator(tokenService, meterRegistry), - new CloudApiKeyAuthenticator(nodeName, cloudApiKeyService), + new PluggableApiKeyAuthenticator(customApiKeyAuthenticator), new ApiKeyAuthenticator(apiKeyService, nodeName, meterRegistry), new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache, meterRegistry) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index 4b49898dc78ca..2f28a3d995690 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -54,7 +54,7 @@ class AuthenticatorChain { AuthenticationContextSerializer authenticationSerializer, ServiceAccountAuthenticator serviceAccountAuthenticator, OAuth2TokenAuthenticator oAuth2TokenAuthenticator, - CloudApiKeyAuthenticator cloudApiKeyAuthenticator, + PluggableApiKeyAuthenticator customApiKeyAuthenticator, ApiKeyAuthenticator apiKeyAuthenticator, RealmsAuthenticator realmsAuthenticator ) { @@ -68,7 +68,7 @@ class AuthenticatorChain { this.allAuthenticators = List.of( serviceAccountAuthenticator, oAuth2TokenAuthenticator, - cloudApiKeyAuthenticator, + customApiKeyAuthenticator, apiKeyAuthenticator, realmsAuthenticator ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java deleted file mode 100644 index 823dadf3b4979..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CloudApiKeyAuthenticator.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.authc; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; -import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKey; -import org.elasticsearch.xpack.core.security.authc.cloud.CloudApiKeyService; - -public class CloudApiKeyAuthenticator implements Authenticator { - private final String nodeName; - private final CloudApiKeyService cloudApiKeyService; - - public CloudApiKeyAuthenticator(String nodeName, CloudApiKeyService cloudApiKeyService) { - this.nodeName = nodeName; - this.cloudApiKeyService = cloudApiKeyService; - } - - @Override - public String name() { - return "cloud API key"; - } - - @Override - public AuthenticationToken extractCredentials(Context context) { - return cloudApiKeyService.parseAsCloudApiKey(context.getApiKeyString()); - } - - @Override - public void authenticate(Context context, ActionListener> listener) { - final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken(); - if (false == authenticationToken instanceof CloudApiKey) { - listener.onResponse(AuthenticationResult.notHandled()); - return; - } - - final CloudApiKey cloudApiKey = (CloudApiKey) authenticationToken; - - cloudApiKeyService.authenticate( - cloudApiKey, - nodeName, - ActionListener.wrap( - authentication -> listener.onResponse(AuthenticationResult.success(authentication)), - e -> listener.onFailure(context.getRequest().exceptionProcessingRequest(e, cloudApiKey)) - ) - ); - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java new file mode 100644 index 0000000000000..d269c8ff46ade --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java @@ -0,0 +1,44 @@ +/* + * 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.authc; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator; + +public class PluggableApiKeyAuthenticator implements Authenticator { + private final CustomApiKeyAuthenticator authenticator; + + public PluggableApiKeyAuthenticator(CustomApiKeyAuthenticator authenticator) { + this.authenticator = authenticator; + } + + @Override + public String name() { + return authenticator.name(); + } + + @Override + public AuthenticationToken extractCredentials(Context context) { + return authenticator.extractCredentials(context.getApiKeyString()); + } + + @Override + public void authenticate(Context context, ActionListener> listener) { + final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken(); + authenticator.authenticate( + authenticationToken, + ActionListener.wrap( + listener::onResponse, + ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken)) + ) + ); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index df1ac79d487b8..bd1e6c74a668c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -67,7 +67,7 @@ public class AuthenticatorChainTests extends ESTestCase { private ServiceAccountAuthenticator serviceAccountAuthenticator; private OAuth2TokenAuthenticator oAuth2TokenAuthenticator; private ApiKeyAuthenticator apiKeyAuthenticator; - private CloudApiKeyAuthenticator cloudApiKeyAuthenticator; + private PluggableApiKeyAuthenticator customApiKeyAuthenticatorWrapper; private RealmsAuthenticator realmsAuthenticator; private Authentication authentication; private User fallbackUser; @@ -92,7 +92,7 @@ public void init() { oAuth2TokenAuthenticator = mock(OAuth2TokenAuthenticator.class); apiKeyAuthenticator = mock(ApiKeyAuthenticator.class); realmsAuthenticator = mock(RealmsAuthenticator.class); - cloudApiKeyAuthenticator = mock(CloudApiKeyAuthenticator.class); + customApiKeyAuthenticatorWrapper = mock(PluggableApiKeyAuthenticator.class); when(realms.getActiveRealms()).thenReturn(List.of(mock(Realm.class))); when(realms.getUnlicensedRealms()).thenReturn(List.of()); final User user = new User(randomAlphaOfLength(8)); @@ -105,7 +105,7 @@ public void init() { authenticationContextSerializer, serviceAccountAuthenticator, oAuth2TokenAuthenticator, - cloudApiKeyAuthenticator, + customApiKeyAuthenticatorWrapper, apiKeyAuthenticator, realmsAuthenticator ); @@ -220,7 +220,7 @@ public void testAuthenticateWithApiKey() throws IOException { new ApiKeyCredentials(randomAlphaOfLength(20), apiKeySecret, randomFrom(ApiKey.Type.values())) ); doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @@ -263,7 +263,7 @@ public void testAuthenticateWithRealms() throws IOException { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") @@ -326,7 +326,7 @@ public void testContextWithDirectWrongTokenFailsAuthn() { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(cloudApiKeyAuthenticator).authenticate(eq(context), anyActionListener()); + doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); // 1. realms do not consume the token doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") From e3abd81744b83920412e14152c89a4b046db81e2 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 15:01:54 +0200 Subject: [PATCH 10/18] More --- .../xpack/security/authc/AuthenticatorChain.java | 4 ++-- .../authc/PluggableApiKeyAuthenticator.java | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index 2f28a3d995690..f9e255c633dcd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -54,7 +54,7 @@ class AuthenticatorChain { AuthenticationContextSerializer authenticationSerializer, ServiceAccountAuthenticator serviceAccountAuthenticator, OAuth2TokenAuthenticator oAuth2TokenAuthenticator, - PluggableApiKeyAuthenticator customApiKeyAuthenticator, + PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator, ApiKeyAuthenticator apiKeyAuthenticator, RealmsAuthenticator realmsAuthenticator ) { @@ -68,7 +68,7 @@ class AuthenticatorChain { this.allAuthenticators = List.of( serviceAccountAuthenticator, oAuth2TokenAuthenticator, - customApiKeyAuthenticator, + pluggableApiKeyAuthenticator, apiKeyAuthenticator, realmsAuthenticator ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java index d269c8ff46ade..4ee8ec7be723d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java @@ -33,12 +33,14 @@ public AuthenticationToken extractCredentials(Context context) { @Override public void authenticate(Context context, ActionListener> listener) { final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken(); - authenticator.authenticate( - authenticationToken, - ActionListener.wrap( - listener::onResponse, - ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken)) - ) - ); + authenticator.authenticate(authenticationToken, ActionListener.wrap(response -> { + if (response.isAuthenticated()) { + listener.onResponse(response); + } else if (response.getStatus() == AuthenticationResult.Status.TERMINATE) { + listener.onFailure(context.getRequest().exceptionProcessingRequest(response.getException(), authenticationToken)); + } else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) { + listener.onResponse(AuthenticationResult.notHandled()); + } + }, ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken)))); } } From 8b0f1d3c2083170e6607dcd0c1660a797380bde8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 15:07:16 +0200 Subject: [PATCH 11/18] Javadoc --- .../security/authc/apikey/CustomApiKeyAuthenticator.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java index c2f79df940cd0..0e44d96d4d620 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java @@ -14,6 +14,11 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +/** + * An extension point to provide a custom API key authenticator implementation. + * The implementation is wrapped by a core Authenticator class and included in the authenticator chain _before_ the + * default API key authenticator. + */ public interface CustomApiKeyAuthenticator { String name(); From ca6efe86758e37ff72045bae662cb45d726cfe45 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 16:18:00 +0200 Subject: [PATCH 12/18] Javadoc --- .../core/security/authc/apikey/CustomApiKeyAuthenticator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java index 0e44d96d4d620..5113e4f095746 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java @@ -26,6 +26,9 @@ public interface CustomApiKeyAuthenticator { void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener> listener); + /** + * A no-op implementation of {@link CustomApiKeyAuthenticator} that does nothing and is effectively skipped in the authenticator chain. + */ class Noop implements CustomApiKeyAuthenticator { @Override public String name() { From 444b9a194ad0e2ade968cbf3b35bff3a4b5feb52 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 16:35:06 +0200 Subject: [PATCH 13/18] Fix tests --- .../authc/AuthenticatorChainTests.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index bd1e6c74a668c..bfd122655768b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -67,7 +67,7 @@ public class AuthenticatorChainTests extends ESTestCase { private ServiceAccountAuthenticator serviceAccountAuthenticator; private OAuth2TokenAuthenticator oAuth2TokenAuthenticator; private ApiKeyAuthenticator apiKeyAuthenticator; - private PluggableApiKeyAuthenticator customApiKeyAuthenticatorWrapper; + private PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator; private RealmsAuthenticator realmsAuthenticator; private Authentication authentication; private User fallbackUser; @@ -92,7 +92,7 @@ public void init() { oAuth2TokenAuthenticator = mock(OAuth2TokenAuthenticator.class); apiKeyAuthenticator = mock(ApiKeyAuthenticator.class); realmsAuthenticator = mock(RealmsAuthenticator.class); - customApiKeyAuthenticatorWrapper = mock(PluggableApiKeyAuthenticator.class); + pluggableApiKeyAuthenticator = mock(PluggableApiKeyAuthenticator.class); when(realms.getActiveRealms()).thenReturn(List.of(mock(Realm.class))); when(realms.getUnlicensedRealms()).thenReturn(List.of()); final User user = new User(randomAlphaOfLength(8)); @@ -105,7 +105,7 @@ public void init() { authenticationContextSerializer, serviceAccountAuthenticator, oAuth2TokenAuthenticator, - customApiKeyAuthenticatorWrapper, + pluggableApiKeyAuthenticator, apiKeyAuthenticator, realmsAuthenticator ); @@ -220,7 +220,13 @@ public void testAuthenticateWithApiKey() throws IOException { new ApiKeyCredentials(randomAlphaOfLength(20), apiKeySecret, randomFrom(ApiKey.Type.values())) ); doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener> listener = (ActionListener< + AuthenticationResult>) invocationOnMock.getArguments()[1]; + listener.onResponse(AuthenticationResult.notHandled()); + return null; + }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @@ -263,7 +269,13 @@ public void testAuthenticateWithRealms() throws IOException { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener> listener = (ActionListener< + AuthenticationResult>) invocationOnMock.getArguments()[1]; + listener.onResponse(AuthenticationResult.notHandled()); + return null; + }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); } doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") @@ -326,7 +338,14 @@ public void testContextWithDirectWrongTokenFailsAuthn() { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doCallRealMethod().when(customApiKeyAuthenticatorWrapper).authenticate(eq(context), anyActionListener()); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener> listener = (ActionListener< + AuthenticationResult>) invocationOnMock.getArguments()[1]; + listener.onResponse(AuthenticationResult.notHandled()); + return null; + }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); + // 1. realms do not consume the token doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") From f868dafa86cf3dac6924d1004c2ffa72ec259858 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 28 May 2025 16:40:43 +0200 Subject: [PATCH 14/18] Exception handling --- .../security/authc/apikey/CustomApiKeyAuthenticator.java | 4 ++-- .../xpack/security/authc/PluggableApiKeyAuthenticator.java | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java index 5113e4f095746..4f5d05e720715 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java @@ -16,7 +16,7 @@ /** * An extension point to provide a custom API key authenticator implementation. - * The implementation is wrapped by a core Authenticator class and included in the authenticator chain _before_ the + * The implementation is wrapped by a core `Authenticator` class and included in the authenticator chain _before_ the * default API key authenticator. */ public interface CustomApiKeyAuthenticator { @@ -27,7 +27,7 @@ public interface CustomApiKeyAuthenticator { void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener> listener); /** - * A no-op implementation of {@link CustomApiKeyAuthenticator} that does nothing and is effectively skipped in the authenticator chain. + * A no-op implementation of {@link CustomApiKeyAuthenticator} that is effectively skipped in the authenticator chain. */ class Noop implements CustomApiKeyAuthenticator { @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java index 4ee8ec7be723d..aee712f258480 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java @@ -37,7 +37,12 @@ public void authenticate(Context context, ActionListener Date: Wed, 28 May 2025 16:43:59 +0200 Subject: [PATCH 15/18] Javadoc --- .../xpack/security/authc/PluggableApiKeyAuthenticator.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java index aee712f258480..0637efbc5e89a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java @@ -13,6 +13,11 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator; +/** + * An adapter for {@link CustomApiKeyAuthenticator} that implements the {@link Authenticator} interface, so the custom API key authenticator + * can be plugged into the authenticator chain. Module dependencies prevent us from introducing a direct extension point for + * an {@link Authenticator}. + */ public class PluggableApiKeyAuthenticator implements Authenticator { private final CustomApiKeyAuthenticator authenticator; From f1965d39173a151fa199e8115de187bac0ac6d23 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 3 Jun 2025 11:09:06 +0200 Subject: [PATCH 16/18] add new transport version --- .../org/elasticsearch/TransportVersions.java | 1 + .../core/security/authc/Authentication.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index d1d9664547223..9d81a2e256078 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -275,6 +275,7 @@ static TransportVersion def(int id) { public static final TransportVersion ESQL_LIMIT_ROW_SIZE = def(9_085_0_00); public static final TransportVersion ESQL_REGEX_MATCH_WITH_CASE_INSENSITIVITY = def(9_086_0_00); public static final TransportVersion IDP_CUSTOM_SAML_ATTRIBUTES = def(9_087_0_00); + public static final TransportVersion SECURITY_CLOUD_API_KEY_REALM_AND_TYPE = def(9_088_0_00); /* * STOP! READ THIS FIRST! No, really, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 04c489d275d9b..fdfe21cd5ab33 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -267,6 +267,16 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion) + "]" ); } + if (isCloudApiKey() && olderVersion.before(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE)) { + throw new IllegalArgumentException( + "versions of Elasticsearch before [" + + TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion() + + "] can't handle cloud API key authentication and attempted to rewrite for [" + + olderVersion.toReleaseVersion() + + "]" + ); + } + final Map newMetadata = maybeRewriteMetadata(olderVersion, this); final Authentication newAuthentication; @@ -632,6 +642,16 @@ private static void doWriteTo(Subject effectiveSubject, Subject authenticatingSu + "]" ); } + if (effectiveSubject.getType() == Subject.Type.CLOUD_API_KEY + && out.getTransportVersion().before(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE)) { + throw new IllegalArgumentException( + "versions of Elasticsearch before [" + + TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion() + + "] can't handle cloud API key authentication and attempted to send to [" + + out.getTransportVersion().toReleaseVersion() + + "]" + ); + } final boolean isRunAs = authenticatingSubject != effectiveSubject; if (isRunAs) { final User outerUser = effectiveSubject.getUser(); From 30dc57d16f6ee9bca72b70d4b1164de95b19a56c Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 3 Jun 2025 11:09:50 +0200 Subject: [PATCH 17/18] add todo to followup in ES-11961 --- .../xpack/core/security/xcontent/XContentUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java index 9f7c9e9c24156..9656237eb5044 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java @@ -128,6 +128,9 @@ private static void addSubjectInfo(XContentBuilder builder, Subject subject) thr } builder.endObject(); } + case CLOUD_API_KEY -> { + // TODO Add cloud API key information here + } } } From bd19d18437a569d55cf650c86dce1a3938924e75 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 3 Jun 2025 12:42:05 +0200 Subject: [PATCH 18/18] test cloud API key authentication serialization --- .../AuthenticationSerializationTests.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java index b7c7681c95d03..095a7ed6c59f9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java @@ -115,6 +115,52 @@ public void testWriteToWithCrossClusterAccessThrowsOnUnsupportedVersion() throws } } + public void testWriteToAndReadFromWithCloudApiKeyAuthentication() throws Exception { + final Authentication authentication = Authentication.newCloudApiKeyAuthentication( + AuthenticationResult.success(new User(randomAlphanumericOfLength(5), "superuser"), Map.of()), + randomAlphanumericOfLength(10) + ); + + assertThat(authentication.isCloudApiKey(), is(true)); + + BytesStreamOutput output = new BytesStreamOutput(); + authentication.writeTo(output); + final Authentication readFrom = new Authentication(output.bytes().streamInput()); + assertThat(readFrom.isCloudApiKey(), is(true)); + + assertThat(readFrom, not(sameInstance(authentication))); + assertThat(readFrom, equalTo(authentication)); + } + + public void testWriteToWithCloudApiKeyThrowsOnUnsupportedVersion() { + final Authentication authentication = Authentication.newCloudApiKeyAuthentication( + AuthenticationResult.success(new User(randomAlphanumericOfLength(5), "superuser"), Map.of()), + randomAlphanumericOfLength(10) + ); + + try (BytesStreamOutput out = new BytesStreamOutput()) { + final TransportVersion version = TransportVersionUtils.randomVersionBetween( + random(), + TransportVersions.V_8_0_0, + TransportVersionUtils.getPreviousVersion(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE) + ); + out.setTransportVersion(version); + + final var ex = expectThrows(IllegalArgumentException.class, () -> authentication.writeTo(out)); + assertThat( + ex.getMessage(), + containsString( + "versions of Elasticsearch before [" + + TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion() + + "] can't handle cloud API key authentication and attempted to send to [" + + out.getTransportVersion().toReleaseVersion() + + "]" + ) + ); + } + + } + public void testSystemUserReadAndWrite() throws Exception { BytesStreamOutput output = new BytesStreamOutput();