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 449246fbb5c92..ba3629d9e9a62 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 @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; +import org.elasticsearch.telemetry.TelemetryProvider; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; @@ -63,6 +64,9 @@ interface SecurityComponents { /** Provides the ability to access project-scoped data from the global scope **/ ProjectResolver projectResolver(); + + /** Provides the ability to access the APM tracer and meter registry **/ + TelemetryProvider telemetryProvider(); } /** 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 ac543c4f81cf3..879d313e0f232 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 @@ -823,6 +823,9 @@ public void toXContentFragment(XContentBuilder builder) throws IOException { apiKeyField.put("managed_by", CredentialManagedBy.CLOUD.getDisplayName()); builder.field("api_key", Collections.unmodifiableMap(apiKeyField)); } + if (metadata.containsKey("managed_by")) { + builder.field("managed_by", metadata.get("managed_by")); + } } public static Authentication getAuthenticationFromCrossClusterAccessMetadata(Authentication authentication) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java index 5415714bb3114..25789bad921f5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java @@ -13,6 +13,7 @@ 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.user.User; /** * An extension point to provide a custom authenticator implementation. For example, a custom API key or a custom OAuth2 @@ -28,4 +29,6 @@ public interface CustomAuthenticator { void authenticate(@Nullable AuthenticationToken token, ActionListener> listener); + Authentication getAuthentication(AuthenticationResult result, String nodeName); + } 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 88b421e1efe31..3fae45a2a7765 100644 --- a/x-pack/plugin/security/src/main/java/module-info.java +++ b/x-pack/plugin/security/src/main/java/module-info.java @@ -76,6 +76,7 @@ exports org.elasticsearch.xpack.security.support to org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authz.store to org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authc.service; + exports org.elasticsearch.xpack.security.metric; provides org.elasticsearch.index.SlowLogFieldProvider with org.elasticsearch.xpack.security.slowlog.SecuritySlowLogFieldProvider; 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 c9f0771e93068..e2a44a12fe812 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 @@ -854,7 +854,8 @@ Collection createComponents( clusterService, resourceWatcherService, userRoleMapper, - projectResolver + projectResolver, + telemetryProvider ); Map realmFactories = new HashMap<>( InternalRealms.getFactories( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java index 41a8412ce4685..e798e8db671fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.core.common.IteratingActionListener; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -15,6 +16,7 @@ import java.util.List; import java.util.Objects; +import java.util.function.BiConsumer; public class PluggableAuthenticatorChain implements Authenticator { @@ -55,28 +57,48 @@ public void authenticate(Context context, ActionListener { - if (response.isAuthenticated()) { - listener.onResponse(response); - } else if (response.getStatus() == AuthenticationResult.Status.TERMINATE) { - final Exception ex = response.getException(); - if (ex == null) { - listener.onFailure(context.getRequest().authenticationFailed(token)); - } else { - listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)); - } - } else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) { - listener.onResponse(AuthenticationResult.notHandled()); - } - }, ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)))); - return; - } - } + var lis = new IteratingActionListener<>( + listener, + getAuthConsumer(context), + customAuthenticators, + context.getThreadContext(), + result -> { + if (result == null) { + // all custom authenticators left the token unhandled + return AuthenticationResult.notHandled(); + } + return result; + }, + result -> result == null || result.getStatus() == AuthenticationResult.Status.CONTINUE + ); + lis.run(); + return; } listener.onResponse(AuthenticationResult.notHandled()); } + private BiConsumer>> getAuthConsumer(Context context) { + AuthenticationToken token = context.getMostRecentAuthenticationToken(); + return (authenticator, iteratingListener) -> { + if (authenticator.supports(token)) { + authenticator.authenticate(token, ActionListener.wrap(response -> { + if (response.isAuthenticated()) { + iteratingListener.onResponse(response); + } else if (response.getStatus() == AuthenticationResult.Status.TERMINATE) { + final Exception ex = response.getException(); + if (ex == null) { + iteratingListener.onFailure(context.getRequest().authenticationFailed(token)); + } else { + iteratingListener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)); + } + } else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) { + iteratingListener.onResponse(AuthenticationResult.notHandled()); + } + }, ex -> iteratingListener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)))); + } else { + iteratingListener.onResponse(null); // try the next custom authenticator + } + }; + } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/InstrumentedSecurityActionListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/InstrumentedSecurityActionListener.java index 101f49258dd59..90ed7bf8312e2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/InstrumentedSecurityActionListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/InstrumentedSecurityActionListener.java @@ -42,4 +42,31 @@ public static ActionListener> wrapForAuthc( }), () -> metrics.recordTime(context, startTimeNano)); } + /** + * A simpler variant that re-uses the Authentication Result as the context. This can be handy in situations when the attributes that are + * of interest are only available after the authentication is completed and not before. + * As a natural consequence, there will be no context available at the point of recording start time and in cases of exceptional failure + * @param metrics + * @param listener + * @param + * @return + */ + public static ActionListener> wrapForAuthc( + final SecurityMetrics> metrics, + final ActionListener> listener + ) { + assert metrics.type().group() == SecurityMetricGroup.AUTHC; + final long startTimeNano = metrics.relativeTimeInNanos(); + return ActionListener.runBefore(ActionListener.wrap(result -> { + if (result.isAuthenticated()) { + metrics.recordSuccess(result); + } else { + metrics.recordFailure(result, result.getMessage()); + } + listener.onResponse(result); + }, e -> { + metrics.recordFailure(null, e.getMessage()); + listener.onFailure(e); + }), () -> metrics.recordTime(null, startTimeNano)); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/SecurityMetricType.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/SecurityMetricType.java index 02ac292aee781..74a9164b104d4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/SecurityMetricType.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/SecurityMetricType.java @@ -19,6 +19,25 @@ public enum SecurityMetricType { new SecurityMetricInfo("es.security.authc.api_key.time", "Time it took (in nanoseconds) to execute API key authentication.", "ns") ), + CLOUD_AUTHC_API_KEY( + SecurityMetricGroup.AUTHC, + new SecurityMetricInfo( + "es.security.authc.cloud_api_key.success.total", + "Number of successful cloud API key authentications.", + "count" + ), + new SecurityMetricInfo( + "es.security.authc.cloud_api_key.failures.total", + "Number of failed cloud API key authentications.", + "count" + ), + new SecurityMetricInfo( + "es.security.authc.cloud_api_key.time", + "Time it took (in nanoseconds) to execute cloud API key authentication.", + "ns" + ) + ), + AUTHC_SERVICE_ACCOUNT( SecurityMetricGroup.AUTHC, new SecurityMetricInfo( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ExtensionComponents.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ExtensionComponents.java index 1e14fe197e724..56c5ab993f8a4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ExtensionComponents.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ExtensionComponents.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; +import org.elasticsearch.telemetry.TelemetryProvider; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.SecurityExtension; @@ -27,6 +28,7 @@ public final class ExtensionComponents implements SecurityExtension.SecurityComp private final ResourceWatcherService resourceWatcherService; private final UserRoleMapper roleMapper; private final ProjectResolver projectResolver; + private final TelemetryProvider telemetryProvider; public ExtensionComponents( Environment environment, @@ -34,7 +36,8 @@ public ExtensionComponents( ClusterService clusterService, ResourceWatcherService resourceWatcherService, UserRoleMapper roleMapper, - ProjectResolver projectResolver + ProjectResolver projectResolver, + TelemetryProvider telemetryProvider ) { this.environment = environment; this.client = client; @@ -42,6 +45,7 @@ public ExtensionComponents( this.resourceWatcherService = resourceWatcherService; this.roleMapper = roleMapper; this.projectResolver = projectResolver; + this.telemetryProvider = telemetryProvider; } @Override @@ -83,4 +87,9 @@ public UserRoleMapper roleMapper() { public ProjectResolver projectResolver() { return projectResolver; } + + @Override + public TelemetryProvider telemetryProvider() { + return telemetryProvider; + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChainTests.java new file mode 100644 index 0000000000000..96cdeee7deadf --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChainTests.java @@ -0,0 +1,354 @@ +/* + * 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.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomAuthenticator; +import org.elasticsearch.xpack.core.security.user.User; +import org.junit.Before; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class PluggableAuthenticatorChainTests extends ESTestCase { + + private ThreadContext threadContext; + + private static class TestTokenA implements AuthenticationToken { + private final String value; + + TestTokenA(String value) { + this.value = value; + } + + @Override + public String principal() { + return "user-" + value; + } + + @Override + public Object credentials() { + return value; + } + + @Override + public void clearCredentials() { + // no-op + } + + public String getValue() { + return value; + } + } + + private static class TestTokenB implements AuthenticationToken { + private final String value; + + TestTokenB(String value) { + this.value = value; + } + + @Override + public String principal() { + return "user-" + value; + } + + @Override + public Object credentials() { + return value; + } + + @Override + public void clearCredentials() { + // no-op + } + + public String getValue() { + return value; + } + } + + public class TokenAAuthenticator implements CustomAuthenticator { + + private final String id; + + public TokenAAuthenticator() { + id = "1"; + } + + public TokenAAuthenticator(String id) { + this.id = id; + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof TestTokenA; + } + + @Override + public @Nullable AuthenticationToken extractToken(ThreadContext context) { + return new TestTokenA("foo"); + } + + @Override + public void authenticate(@Nullable AuthenticationToken token, ActionListener> listener) { + if (token instanceof TestTokenA testToken) { + User user = new User("token-a-auth-user-" + id + "-" + testToken.getValue()); + Authentication auth = AuthenticationTestHelper.builder().user(user).build(false); + listener.onResponse(AuthenticationResult.success(auth)); + } else { + listener.onResponse(AuthenticationResult.notHandled()); + } + } + + @Override + public Authentication getAuthentication(AuthenticationResult result, String nodeName) { + return AuthenticationTestHelper.builder().user(result.getValue()).build(false); + } + } + + public class TokenBAuthenticator implements CustomAuthenticator { + + private final String id; + + public TokenBAuthenticator() { + id = "1"; + } + + public TokenBAuthenticator(String id) { + this.id = id; + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof TestTokenB; + } + + @Override + public @Nullable AuthenticationToken extractToken(ThreadContext context) { + return new TestTokenB("foo"); + } + + @Override + public void authenticate(@Nullable AuthenticationToken token, ActionListener> listener) { + if (token instanceof TestTokenB testToken) { + User user = new User("token-b-auth-user-" + id + "-" + testToken.getValue()); + Authentication auth = AuthenticationTestHelper.builder().user(user).build(false); + listener.onResponse(AuthenticationResult.success(auth)); + } else { + listener.onResponse(AuthenticationResult.notHandled()); + } + } + + @Override + public Authentication getAuthentication(AuthenticationResult result, String nodeName) { + return AuthenticationTestHelper.builder().user(result.getValue()).build(false); + } + } + + @Before + public void init() { + final Settings settings = Settings.builder().build(); + threadContext = new ThreadContext(settings); + } + + public void testAuthenticateWithTokenAPickedUpByTokenAAuthenticatorInCustomChain() throws Exception { + + PluggableAuthenticatorChain chain = new PluggableAuthenticatorChain(List.of(new TokenAAuthenticator(), new TokenBAuthenticator())); + TestTokenA testToken = new TestTokenA("test-value"); + + Authenticator.Context context = createContext(); + context.addAuthenticationToken(testToken); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> resultRef = new AtomicReference<>(); + AtomicReference exceptionRef = new AtomicReference<>(); + + ActionListener> listener = new ActionListener<>() { + @Override + public void onResponse(AuthenticationResult result) { + resultRef.set(result); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exceptionRef.set(e); + latch.countDown(); + } + }; + + chain.authenticate(context, listener); + latch.await(); + + if (exceptionRef.get() != null) { + throw new AssertionError("Authentication failed with exception", exceptionRef.get()); + } + + AuthenticationResult result = resultRef.get(); + assertThat(result, notNullValue()); + assertThat(result.isAuthenticated(), equalTo(true)); + + Authentication auth = result.getValue(); + assertThat(auth.getEffectiveSubject().getUser().principal(), equalTo("token-a-auth-user-1-test-value")); + } + + public void testAuthenticateWithTokenAPickedUpByTokenAAuthenticatorInCustomChainWithChainOrderFlipped() throws Exception { + + PluggableAuthenticatorChain chain = new PluggableAuthenticatorChain(List.of(new TokenBAuthenticator(), new TokenAAuthenticator())); + TestTokenA testToken = new TestTokenA("test-value"); + + Authenticator.Context context = createContext(); + context.addAuthenticationToken(testToken); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> resultRef = new AtomicReference<>(); + AtomicReference exceptionRef = new AtomicReference<>(); + + ActionListener> listener = new ActionListener<>() { + @Override + public void onResponse(AuthenticationResult result) { + resultRef.set(result); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exceptionRef.set(e); + latch.countDown(); + } + }; + + chain.authenticate(context, listener); + latch.await(); + + if (exceptionRef.get() != null) { + throw new AssertionError("Authentication failed with exception", exceptionRef.get()); + } + + AuthenticationResult result = resultRef.get(); + assertThat(result, notNullValue()); + assertThat(result.isAuthenticated(), equalTo(true)); + + Authentication auth = result.getValue(); + assertThat(auth.getEffectiveSubject().getUser().principal(), equalTo("token-a-auth-user-1-test-value")); + } + + public void testAuthenticateWhenTokenSupportedByBothAuthenticatorsInChain() throws Exception { + + PluggableAuthenticatorChain chain = new PluggableAuthenticatorChain( + List.of(new TokenAAuthenticator("foo"), new TokenAAuthenticator("bar")) + ); + TestTokenA testToken = new TestTokenA("test-value"); + + Authenticator.Context context = createContext(); + context.addAuthenticationToken(testToken); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> resultRef = new AtomicReference<>(); + AtomicReference exceptionRef = new AtomicReference<>(); + + ActionListener> listener = new ActionListener<>() { + @Override + public void onResponse(AuthenticationResult result) { + resultRef.set(result); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exceptionRef.set(e); + latch.countDown(); + } + }; + + chain.authenticate(context, listener); + latch.await(); + + if (exceptionRef.get() != null) { + throw new AssertionError("Authentication failed with exception", exceptionRef.get()); + } + + AuthenticationResult result = resultRef.get(); + assertThat(result, notNullValue()); + assertThat(result.isAuthenticated(), equalTo(true)); + + Authentication auth = result.getValue(); + assertThat(auth.getEffectiveSubject().getUser().principal(), equalTo("token-a-auth-user-foo-test-value")); // id of first + } + + public void testAuthenticateWhenTokenSupportedByNoAuthenticatorsInChain() throws Exception { + + PluggableAuthenticatorChain chain = new PluggableAuthenticatorChain( + List.of(new TokenAAuthenticator("foo"), new TokenAAuthenticator("bar")) + ); + AuthenticationToken unknownToken = new AuthenticationToken() { + @Override + public String principal() { + return "unknown"; + } + + @Override + public Object credentials() { + return null; + } + + @Override + public void clearCredentials() { + // no-op + } + }; + + Authenticator.Context context = createContext(); + context.addAuthenticationToken(unknownToken); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> resultRef = new AtomicReference<>(); + AtomicReference exceptionRef = new AtomicReference<>(); + + ActionListener> listener = new ActionListener<>() { + @Override + public void onResponse(AuthenticationResult result) { + resultRef.set(result); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exceptionRef.set(e); + latch.countDown(); + } + }; + + chain.authenticate(context, listener); + latch.await(); + + if (exceptionRef.get() != null) { + throw new AssertionError("Authentication failed with exception", exceptionRef.get()); + } + + AuthenticationResult result = resultRef.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + } + + private Authenticator.Context createContext() { + return new Authenticator.Context(threadContext, null, null, true, null); + } +}