diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java index 2e527dba980b9..686e24b6abd3d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java @@ -47,7 +47,7 @@ import org.elasticsearch.xpack.core.security.transport.SecurityTransportExceptionHandler; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.SslProfile; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessAuthenticationService; +import org.elasticsearch.xpack.security.authc.RemoteClusterAuthenticationService; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -79,7 +79,7 @@ public class SecurityNetty4Transport extends Netty4Transport { private final boolean remoteClusterServerSslEnabled; private final SslProfile remoteClusterClientSslProfile; private final RemoteClusterClientBootstrapOptions remoteClusterClientBootstrapOptions; - private final CrossClusterAccessAuthenticationService crossClusterAccessAuthenticationService; + private final RemoteClusterAuthenticationService remoteClusterAuthenticationService; public SecurityNetty4Transport( final Settings settings, @@ -91,7 +91,7 @@ public SecurityNetty4Transport( final CircuitBreakerService circuitBreakerService, final SSLService sslService, final SharedGroupFactory sharedGroupFactory, - final CrossClusterAccessAuthenticationService crossClusterAccessAuthenticationService + final RemoteClusterAuthenticationService remoteClusterAuthenticationService ) { super( settings, @@ -103,7 +103,7 @@ public SecurityNetty4Transport( circuitBreakerService, sharedGroupFactory ); - this.crossClusterAccessAuthenticationService = crossClusterAccessAuthenticationService; + this.remoteClusterAuthenticationService = remoteClusterAuthenticationService; this.exceptionHandler = new SecurityTransportExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); this.sslService = sslService; this.transportSslEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); @@ -180,7 +180,7 @@ protected void headerReceived(Header header) { channel.config().setAutoRead(false); // this prevents thread-context changes to propagate beyond the validation, as netty worker threads are reused try (ThreadContext.StoredContext ignore = threadPool.getThreadContext().newStoredContext()) { - crossClusterAccessAuthenticationService.tryAuthenticate( + remoteClusterAuthenticationService.authenticateHeaders( header.getRequestHeaders(), ActionListener.runAfter(ActionListener.wrap(aVoid -> { // authn is successful -> NOOP (the complete request will be subsequently authn & authz & audited) 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 013a29a80f738..20de24ef36cbc 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 @@ -417,6 +417,8 @@ import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import org.elasticsearch.xpack.security.support.SecurityMigrations; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; +import org.elasticsearch.xpack.security.transport.CrossClusterAccessTransportInterceptor; +import org.elasticsearch.xpack.security.transport.RemoteClusterTransportInterceptor; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; @@ -1164,17 +1166,24 @@ Collection createComponents( DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings()); crossClusterAccessAuthcService.set(new CrossClusterAccessAuthenticationService(clusterService, apiKeyService, authcService.get())); components.add(crossClusterAccessAuthcService.get()); + + RemoteClusterTransportInterceptor remoteClusterTransportInterceptor = new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + authcService.get(), + authzService, + securityContext.get(), + crossClusterAccessAuthcService.get(), + getLicenseState() + ); securityInterceptor.set( new SecurityServerTransportInterceptor( settings, threadPool, - authcService.get(), - authzService, getSslService(), securityContext.get(), destructiveOperations, - crossClusterAccessAuthcService.get(), - getLicenseState() + remoteClusterTransportInterceptor ) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java index 16f67c9077311..8fc912d40dfb5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java @@ -34,7 +34,7 @@ import static org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY; import static org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders.CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY; -public class CrossClusterAccessAuthenticationService { +public class CrossClusterAccessAuthenticationService implements RemoteClusterAuthenticationService { private static final Logger logger = LogManager.getLogger(CrossClusterAccessAuthenticationService.class); @@ -52,6 +52,7 @@ public CrossClusterAccessAuthenticationService( this.authenticationService = authenticationService; } + @Override public void authenticate(final String action, final TransportRequest request, final ActionListener listener) { final ThreadContext threadContext = clusterService.threadPool().getThreadContext(); final CrossClusterAccessHeaders crossClusterAccessHeaders; @@ -117,7 +118,8 @@ public void authenticate(final String action, final TransportRequest request, fi } } - public void tryAuthenticate(Map headers, ActionListener listener) { + @Override + public void authenticateHeaders(Map headers, ActionListener listener) { final ApiKeyService.ApiKeyCredentials credentials; try { credentials = extractApiKeyCredentialsFromHeaders(headers); @@ -128,7 +130,8 @@ public void tryAuthenticate(Map headers, ActionListener li tryAuthenticate(credentials, listener); } - public void tryAuthenticate(ApiKeyService.ApiKeyCredentials credentials, ActionListener listener) { + // package-private for testing + void tryAuthenticate(ApiKeyService.ApiKeyCredentials credentials, ActionListener listener) { Objects.requireNonNull(credentials); apiKeyService.tryAuthenticate(clusterService.threadPool().getThreadContext(), credentials, ActionListener.wrap(authResult -> { if (authResult.isAuthenticated()) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RemoteClusterAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RemoteClusterAuthenticationService.java new file mode 100644 index 0000000000000..68fcf74f7487b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RemoteClusterAuthenticationService.java @@ -0,0 +1,45 @@ +/* + * 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.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; + +import java.util.Map; + +/** + * Service interface for authenticating remote cluster requests. + * + *

+ * This service handles authentication for cross-cluster requests. + * It provides methods to authenticate both full transport requests + * and credential headers only. + */ +public interface RemoteClusterAuthenticationService { + + /** + * Called to authenticates a remote cluster transport request. + * + * @param action the transport action being performed + * @param request the transport request containing authentication headers + * @param listener callback to receive the authenticated {@link Authentication} + * object on success, or an exception on failure + */ + void authenticate(String action, TransportRequest request, ActionListener listener); + + /** + * Called early (after transport headers were received) to authenticate a remote cluster transport request. + * + * @param headers map of request headers containing authentication credentials + * @param listener callback to receive {@code null} on successful authentication, + * or an exception on authentication failure + */ + void authenticateHeaders(Map headers, ActionListener listener); + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java index 221b7a65e1f8f..9dc81635562bc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.Map; -import java.util.Optional; import java.util.Set; public final class PreAuthorizationUtils { @@ -118,9 +117,9 @@ private static boolean shouldPreAuthorizeChildActionOfParent(final String parent } public static boolean shouldRemoveParentAuthorizationFromThreadContext( - Optional remoteClusterAlias, String childAction, - SecurityContext securityContext + SecurityContext securityContext, + boolean isRemoteClusterRequest ) { final ParentActionAuthorization parentAuthorization = securityContext.getParentAuthorization(); if (parentAuthorization == null) { @@ -128,7 +127,7 @@ public static boolean shouldRemoveParentAuthorizationFromThreadContext( return false; } - if (remoteClusterAlias.isPresent()) { + if (isRemoteClusterRequest) { // We never want to send the parent authorization header to remote clusters. return true; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterAccessTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterAccessTransportInterceptor.java new file mode 100644 index 0000000000000..c58e75da77a6d --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterAccessTransportInterceptor.java @@ -0,0 +1,388 @@ +/* + * 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.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.support.DestructiveOperations; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.SslConfiguration; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.tasks.TaskCancellationService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteConnectionManager; +import org.elasticsearch.transport.RemoteConnectionManager.RemoteClusterAliasWithCredentials; +import org.elasticsearch.transport.SendRequestTransportException; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo; +import org.elasticsearch.xpack.core.security.user.InternalUser; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.core.ssl.SslProfile; +import org.elasticsearch.xpack.security.Security; +import org.elasticsearch.xpack.security.action.SecurityActionMapper; +import org.elasticsearch.xpack.security.audit.AuditUtil; +import org.elasticsearch.xpack.security.authc.ApiKeyService; +import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.CrossClusterAccessAuthenticationService; +import org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders; +import org.elasticsearch.xpack.security.authz.AuthorizationService; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_PROFILE; +import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_SERVER_ENABLED; +import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY; + +public class CrossClusterAccessTransportInterceptor implements RemoteClusterTransportInterceptor { + + private static final Logger logger = LogManager.getLogger(CrossClusterAccessTransportInterceptor.class); + + private static final Map RCS_INTERNAL_ACTIONS_REPLACEMENTS = Map.of( + "internal:admin/ccr/restore/session/put", + "indices:internal/admin/ccr/restore/session/put", + "internal:admin/ccr/restore/session/clear", + "indices:internal/admin/ccr/restore/session/clear", + "internal:admin/ccr/restore/file_chunk/get", + "indices:internal/admin/ccr/restore/file_chunk/get", + "internal:data/read/esql/open_exchange", + "cluster:internal:data/read/esql/open_exchange", + "internal:data/read/esql/exchange", + "cluster:internal:data/read/esql/exchange", + TaskCancellationService.BAN_PARENT_ACTION_NAME, + TaskCancellationService.REMOTE_CLUSTER_BAN_PARENT_ACTION_NAME, + TaskCancellationService.CANCEL_CHILD_ACTION_NAME, + TaskCancellationService.REMOTE_CLUSTER_CANCEL_CHILD_ACTION_NAME + ); + + private final Function> remoteClusterCredentialsResolver; + private final CrossClusterAccessAuthenticationService crossClusterAccessAuthcService; + private final AuthenticationService authcService; + private final AuthorizationService authzService; + private final XPackLicenseState licenseState; + private final SecurityContext securityContext; + private final ThreadPool threadPool; + private final Settings settings; + + public CrossClusterAccessTransportInterceptor( + Settings settings, + ThreadPool threadPool, + AuthenticationService authcService, + AuthorizationService authzService, + SecurityContext securityContext, + CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, + XPackLicenseState licenseState + ) { + this( + settings, + threadPool, + authcService, + authzService, + securityContext, + crossClusterAccessAuthcService, + licenseState, + RemoteConnectionManager::resolveRemoteClusterAliasWithCredentials + ); + } + + // package-protected for testing + CrossClusterAccessTransportInterceptor( + Settings settings, + ThreadPool threadPool, + AuthenticationService authcService, + AuthorizationService authzService, + SecurityContext securityContext, + CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, + XPackLicenseState licenseState, + Function> remoteClusterCredentialsResolver + ) { + this.remoteClusterCredentialsResolver = remoteClusterCredentialsResolver; + this.crossClusterAccessAuthcService = crossClusterAccessAuthcService; + this.authcService = authcService; + this.authzService = authzService; + this.licenseState = licenseState; + this.securityContext = securityContext; + this.threadPool = threadPool; + this.settings = settings; + } + + @Override + public TransportInterceptor.AsyncSender interceptSender(TransportInterceptor.AsyncSender sender) { + return new TransportInterceptor.AsyncSender() { + @Override + public void sendRequest( + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + final Optional remoteClusterCredentials = getRemoteClusterCredentials(connection); + if (remoteClusterCredentials.isPresent()) { + sendWithCrossClusterAccessHeaders(remoteClusterCredentials.get(), connection, action, request, options, handler); + } else { + // Send regular request, without cross cluster access headers + try { + sender.sendRequest(connection, action, request, options, handler); + } catch (Exception e) { + handler.handleException(new SendRequestTransportException(connection.getNode(), action, e)); + } + } + } + + /** + * Returns cluster credentials if the connection is remote, and cluster credentials are set up for the target cluster. + */ + private Optional getRemoteClusterCredentials(Transport.Connection connection) { + final Optional remoteClusterAliasWithCredentials = remoteClusterCredentialsResolver + .apply(connection); + if (remoteClusterAliasWithCredentials.isEmpty()) { + logger.trace("Connection is not remote"); + return Optional.empty(); + } + + final String remoteClusterAlias = remoteClusterAliasWithCredentials.get().clusterAlias(); + final SecureString remoteClusterCredentials = remoteClusterAliasWithCredentials.get().credentials(); + if (remoteClusterCredentials == null) { + logger.trace("No cluster credentials are configured for remote cluster [{}]", remoteClusterAlias); + return Optional.empty(); + } + + return Optional.of( + new RemoteClusterCredentials(remoteClusterAlias, ApiKeyService.withApiKeyPrefix(remoteClusterCredentials.toString())) + ); + } + + private void sendWithCrossClusterAccessHeaders( + final RemoteClusterCredentials remoteClusterCredentials, + final Transport.Connection connection, + final String action, + final TransportRequest request, + final TransportRequestOptions options, + final TransportResponseHandler handler + ) { + if (false == Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.check(licenseState)) { + throw LicenseUtils.newComplianceException(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.getName()); + } + final String remoteClusterAlias = remoteClusterCredentials.clusterAlias(); + + if (connection.getTransportVersion().before(TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY)) { + throw illegalArgumentExceptionWithDebugLog( + "Settings for remote cluster [" + + remoteClusterAlias + + "] indicate cross cluster access headers should be sent but target cluster version [" + + connection.getTransportVersion().toReleaseVersion() + + "] does not support receiving them" + ); + } + + logger.trace( + () -> format( + "Sending [%s] request for [%s] action to [%s] with cross cluster access headers", + request.getClass(), + action, + remoteClusterAlias + ) + ); + + final Authentication authentication = securityContext.getAuthentication(); + assert authentication != null : "authentication must be present in security context"; + + final User user = authentication.getEffectiveSubject().getUser(); + if (user instanceof InternalUser && false == SystemUser.is(user)) { + final String message = "Internal user [" + user.principal() + "] should not be used for cross cluster requests"; + assert false : message; + throw illegalArgumentExceptionWithDebugLog(message); + } else if (SystemUser.is(user) || action.equals(ClusterStateAction.NAME)) { + if (SystemUser.is(user)) { + logger.trace( + "Request [{}] for action [{}] towards [{}] initiated by the system user. " + + "Sending request with internal cross cluster access user headers", + request.getClass(), + action, + remoteClusterAlias + ); + } else { + // Use system user for cluster state requests (CCR has many calls of cluster state with end-user context) + logger.trace( + () -> format( + "Switching to the system user for cluster state action towards [{}]. Original user is [%s]", + remoteClusterAlias, + user + ) + ); + } + final var crossClusterAccessHeaders = new CrossClusterAccessHeaders( + remoteClusterCredentials.credentials(), + SystemUser.crossClusterAccessSubjectInfo( + authentication.getEffectiveSubject().getTransportVersion(), + authentication.getEffectiveSubject().getRealm().getNodeName() + ) + ); + // To be able to enforce index-level privileges under the new remote cluster security model, + // we switch from old-style internal actions to their new equivalent indices actions so that + // they will be checked for index privileges against the index specified in the requests + final String effectiveAction = RCS_INTERNAL_ACTIONS_REPLACEMENTS.getOrDefault(action, action); + if (false == effectiveAction.equals(action)) { + logger.trace("switching internal action from [{}] to [{}]", action, effectiveAction); + } + sendWithCrossClusterAccessHeaders(crossClusterAccessHeaders, connection, effectiveAction, request, options, handler); + } else { + assert false == action.startsWith("internal:") : "internal action must be sent with system user"; + authzService.getRoleDescriptorsIntersectionForRemoteCluster( + remoteClusterAlias, + connection.getTransportVersion(), + authentication.getEffectiveSubject(), + ActionListener.wrap(roleDescriptorsIntersection -> { + logger.trace( + () -> format( + "Subject [%s] has role descriptors intersection [%s] for action [%s] towards remote cluster [%s]", + authentication.getEffectiveSubject(), + roleDescriptorsIntersection, + action, + remoteClusterAlias + ) + ); + if (roleDescriptorsIntersection.isEmpty()) { + throw authzService.remoteActionDenied( + authentication, + SecurityActionMapper.action(action, request), + remoteClusterAlias + ); + } + final var crossClusterAccessHeaders = new CrossClusterAccessHeaders( + remoteClusterCredentials.credentials(), + new CrossClusterAccessSubjectInfo(authentication, roleDescriptorsIntersection) + ); + sendWithCrossClusterAccessHeaders(crossClusterAccessHeaders, connection, action, request, options, handler); + }, // it's safe to not use a context restore handler here since `getRoleDescriptorsIntersectionForRemoteCluster` + // uses a context preserving listener internally, and `sendWithCrossClusterAccessHeaders` uses a context restore + // handler + e -> handler.handleException(new SendRequestTransportException(connection.getNode(), action, e)) + ) + ); + } + } + + private void sendWithCrossClusterAccessHeaders( + final CrossClusterAccessHeaders crossClusterAccessHeaders, + final Transport.Connection connection, + final String action, + final TransportRequest request, + final TransportRequestOptions options, + final TransportResponseHandler handler + ) { + final ThreadContext threadContext = securityContext.getThreadContext(); + final var contextRestoreHandler = new TransportService.ContextRestoreResponseHandler<>( + threadContext.newRestorableContext(true), + handler + ); + try (ThreadContext.StoredContext ignored = threadContext.stashContextPreservingRequestHeaders(AuditUtil.AUDIT_REQUEST_ID)) { + crossClusterAccessHeaders.writeToContext(threadContext); + sender.sendRequest(connection, action, request, options, contextRestoreHandler); + } catch (Exception e) { + contextRestoreHandler.handleException(new SendRequestTransportException(connection.getNode(), action, e)); + } + } + + private static IllegalArgumentException illegalArgumentExceptionWithDebugLog(String message) { + logger.debug(message); + return new IllegalArgumentException(message); + } + }; + } + + @Override + public boolean isRemoteClusterConnection(Transport.Connection connection) { + return remoteClusterCredentialsResolver.apply(connection).map(RemoteClusterAliasWithCredentials::clusterAlias).isPresent(); + } + + @Override + public Map getProfileTransportFilters( + Map profileConfigurations, + DestructiveOperations destructiveOperations + ) { + Map profileFilters = Maps.newMapWithExpectedSize(profileConfigurations.size() + 1); + + final boolean transportSSLEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + final boolean remoteClusterPortEnabled = REMOTE_CLUSTER_SERVER_ENABLED.get(settings); + final boolean remoteClusterServerSSLEnabled = XPackSettings.REMOTE_CLUSTER_SERVER_SSL_ENABLED.get(settings); + + for (Map.Entry entry : profileConfigurations.entrySet()) { + final String profileName = entry.getKey(); + final SslProfile sslProfile = entry.getValue(); + final SslConfiguration profileConfiguration = sslProfile.configuration(); + assert profileConfiguration != null : "Ssl Profile [" + sslProfile + "] for [" + profileName + "] has a null configuration"; + final boolean useRemoteClusterProfile = remoteClusterPortEnabled && profileName.equals(REMOTE_CLUSTER_PROFILE); + if (useRemoteClusterProfile) { + profileFilters.put( + profileName, + new CrossClusterAccessServerTransportFilter( + crossClusterAccessAuthcService, + authzService, + threadPool.getThreadContext(), + remoteClusterServerSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration), + destructiveOperations, + securityContext, + licenseState + ) + ); + } else { + profileFilters.put( + profileName, + new ServerTransportFilter( + authcService, + authzService, + threadPool.getThreadContext(), + transportSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration), + destructiveOperations, + securityContext + ) + ); + } + } + + return Collections.unmodifiableMap(profileFilters); + } + + @Override + public boolean hasRemoteClusterAccessHeadersInContext(SecurityContext securityContext) { + return securityContext.getThreadContext().getHeader(CrossClusterAccessHeaders.CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY) != null + || securityContext.getThreadContext() + .getHeader(CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY) != null; + } + + record RemoteClusterCredentials(String clusterAlias, String credentials) { + + @Override + public String toString() { + return "RemoteClusterCredentials{clusterAlias='" + clusterAlias + "', credentials='::es_redacted::'}"; + } + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/RemoteClusterTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/RemoteClusterTransportInterceptor.java new file mode 100644 index 0000000000000..5328c280a629f --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/RemoteClusterTransportInterceptor.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.transport; + +import org.elasticsearch.action.support.DestructiveOperations; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.ssl.SslProfile; + +import java.util.Map; + +/** + * Allows to provide remote cluster interception that's capable of intercepting remote connections + * both on the receiver and the sender side. + */ +public interface RemoteClusterTransportInterceptor { + + /** + * Allows to intercept all transport requests on the sender side. + */ + TransportInterceptor.AsyncSender interceptSender(TransportInterceptor.AsyncSender sender); + + /** + * This method returns {@code true} if the outbound {@code connection} is targeting a remote cluster. + */ + boolean isRemoteClusterConnection(Transport.Connection connection); + + /** + * Allows interceptors to provide a custom {@link ServerTransportFilter} implementations per transport profile. + * The transport filter is called on the receiver side to filter incoming requests + * and execute authentication and authorization for all requests. + * + * @return map of {@link ServerTransportFilter}s per transport profile name + */ + Map getProfileTransportFilters( + Map profileConfigurations, + DestructiveOperations destructiveOperations + ); + + /** + * Returns {@code true} if any of the remote cluster access headers are in the security context. + * This method is used to assert we don't have access headers already in the security context, + * before we even run remote cluster intercepts. Serves as an integrity check that we properly clear + * the security context between requests. + */ + boolean hasRemoteClusterAccessHeadersInContext(SecurityContext securityContext); + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index fb56a9f46cc4b..d5f802ae0f1d1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -10,23 +10,14 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.support.DestructiveOperations; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.ssl.SslConfiguration; -import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.RunOnce; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.license.LicenseUtils; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.tasks.Task; -import org.elasticsearch.tasks.TaskCancellationService; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.RemoteConnectionManager; -import org.elasticsearch.transport.RemoteConnectionManager.RemoteClusterAliasWithCredentials; import org.elasticsearch.transport.SendRequestTransportException; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportChannel; @@ -38,122 +29,49 @@ import org.elasticsearch.transport.TransportResponseHandler; import org.elasticsearch.transport.TransportService; import org.elasticsearch.transport.TransportService.ContextRestoreResponseHandler; -import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo; import org.elasticsearch.xpack.core.security.transport.ProfileConfigurations; -import org.elasticsearch.xpack.core.security.user.InternalUser; -import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.SslProfile; -import org.elasticsearch.xpack.security.Security; -import org.elasticsearch.xpack.security.action.SecurityActionMapper; -import org.elasticsearch.xpack.security.audit.AuditUtil; -import org.elasticsearch.xpack.security.authc.ApiKeyService; -import org.elasticsearch.xpack.security.authc.AuthenticationService; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessAuthenticationService; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders; -import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationUtils; import org.elasticsearch.xpack.security.authz.PreAuthorizationUtils; -import java.util.Collections; import java.util.Map; -import java.util.Optional; import java.util.concurrent.Executor; -import java.util.function.Function; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_PROFILE; -import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_SERVER_ENABLED; -import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY; public class SecurityServerTransportInterceptor implements TransportInterceptor { private static final Logger logger = LogManager.getLogger(SecurityServerTransportInterceptor.class); - private static final Map RCS_INTERNAL_ACTIONS_REPLACEMENTS = Map.of( - "internal:admin/ccr/restore/session/put", - "indices:internal/admin/ccr/restore/session/put", - "internal:admin/ccr/restore/session/clear", - "indices:internal/admin/ccr/restore/session/clear", - "internal:admin/ccr/restore/file_chunk/get", - "indices:internal/admin/ccr/restore/file_chunk/get", - "internal:data/read/esql/open_exchange", - "cluster:internal:data/read/esql/open_exchange", - "internal:data/read/esql/exchange", - "cluster:internal:data/read/esql/exchange", - TaskCancellationService.BAN_PARENT_ACTION_NAME, - TaskCancellationService.REMOTE_CLUSTER_BAN_PARENT_ACTION_NAME, - TaskCancellationService.CANCEL_CHILD_ACTION_NAME, - TaskCancellationService.REMOTE_CLUSTER_CANCEL_CHILD_ACTION_NAME - ); - - private final AuthenticationService authcService; - private final AuthorizationService authzService; - private final SSLService sslService; + + private final RemoteClusterTransportInterceptor remoteClusterTransportInterceptor; private final Map profileFilters; private final ThreadPool threadPool; - private final Settings settings; private final SecurityContext securityContext; - private final CrossClusterAccessAuthenticationService crossClusterAccessAuthcService; - private final Function> remoteClusterCredentialsResolver; - private final XPackLicenseState licenseState; public SecurityServerTransportInterceptor( Settings settings, ThreadPool threadPool, - AuthenticationService authcService, - AuthorizationService authzService, SSLService sslService, SecurityContext securityContext, DestructiveOperations destructiveOperations, - CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, - XPackLicenseState licenseState - ) { - this( - settings, - threadPool, - authcService, - authzService, - sslService, - securityContext, - destructiveOperations, - crossClusterAccessAuthcService, - licenseState, - RemoteConnectionManager::resolveRemoteClusterAliasWithCredentials - ); - } + RemoteClusterTransportInterceptor remoteClusterTransportInterceptor - SecurityServerTransportInterceptor( - Settings settings, - ThreadPool threadPool, - AuthenticationService authcService, - AuthorizationService authzService, - SSLService sslService, - SecurityContext securityContext, - DestructiveOperations destructiveOperations, - CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, - XPackLicenseState licenseState, - // Inject for simplified testing - Function> remoteClusterCredentialsResolver ) { - this.settings = settings; - this.threadPool = threadPool; - this.authcService = authcService; - this.authzService = authzService; - this.sslService = sslService; + this.remoteClusterTransportInterceptor = remoteClusterTransportInterceptor; this.securityContext = securityContext; - this.crossClusterAccessAuthcService = crossClusterAccessAuthcService; - this.licenseState = licenseState; - this.remoteClusterCredentialsResolver = remoteClusterCredentialsResolver; - this.profileFilters = initializeProfileFilters(destructiveOperations); + this.threadPool = threadPool; + final Map profileConfigurations = ProfileConfigurations.get(settings, sslService, false); + this.profileFilters = this.remoteClusterTransportInterceptor.getProfileTransportFilters( + profileConfigurations, + destructiveOperations + ); } @Override public AsyncSender interceptSender(AsyncSender sender) { - return interceptForAllRequests(interceptForCrossClusterAccessRequests(sender)); + return interceptForAllRequests(remoteClusterTransportInterceptor.interceptSender(sender)); } private AsyncSender interceptForAllRequests(AsyncSender sender) { @@ -166,10 +84,14 @@ public void sendRequest( TransportRequestOptions options, TransportResponseHandler handler ) { - assertNoCrossClusterAccessHeadersInContext(); - final Optional remoteClusterAlias = remoteClusterCredentialsResolver.apply(connection) - .map(RemoteClusterAliasWithCredentials::clusterAlias); - if (PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext(remoteClusterAlias, action, securityContext)) { + assert false == remoteClusterTransportInterceptor.hasRemoteClusterAccessHeadersInContext(securityContext) + : "remote cluster access headers should not be in security context"; + final boolean isRemoteClusterConnection = remoteClusterTransportInterceptor.isRemoteClusterConnection(connection); + if (PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext( + action, + securityContext, + isRemoteClusterConnection + )) { securityContext.executeAfterRemovingParentAuthorization(original -> { sendRequestInner( sender, @@ -184,15 +106,6 @@ public void sendRequest( sendRequestInner(sender, connection, action, request, options, handler); } } - - private void assertNoCrossClusterAccessHeadersInContext() { - assert securityContext.getThreadContext() - .getHeader(CrossClusterAccessHeaders.CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY) == null - : "cross cluster access headers should not be in security context"; - assert securityContext.getThreadContext() - .getHeader(CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY) == null - : "cross cluster access headers should not be in security context"; - } }; } @@ -260,188 +173,6 @@ Map getProfileFilters() { return profileFilters; } - private AsyncSender interceptForCrossClusterAccessRequests(final AsyncSender sender) { - return new AsyncSender() { - @Override - public void sendRequest( - Transport.Connection connection, - String action, - TransportRequest request, - TransportRequestOptions options, - TransportResponseHandler handler - ) { - final Optional remoteClusterCredentials = getRemoteClusterCredentials(connection); - if (remoteClusterCredentials.isPresent()) { - sendWithCrossClusterAccessHeaders(remoteClusterCredentials.get(), connection, action, request, options, handler); - } else { - // Send regular request, without cross cluster access headers - try { - sender.sendRequest(connection, action, request, options, handler); - } catch (Exception e) { - handler.handleException(new SendRequestTransportException(connection.getNode(), action, e)); - } - } - } - - /** - * Returns cluster credentials if the connection is remote, and cluster credentials are set up for the target cluster. - */ - private Optional getRemoteClusterCredentials(Transport.Connection connection) { - final Optional remoteClusterAliasWithCredentials = remoteClusterCredentialsResolver - .apply(connection); - if (remoteClusterAliasWithCredentials.isEmpty()) { - logger.trace("Connection is not remote"); - return Optional.empty(); - } - - final String remoteClusterAlias = remoteClusterAliasWithCredentials.get().clusterAlias(); - final SecureString remoteClusterCredentials = remoteClusterAliasWithCredentials.get().credentials(); - if (remoteClusterCredentials == null) { - logger.trace("No cluster credentials are configured for remote cluster [{}]", remoteClusterAlias); - return Optional.empty(); - } - - return Optional.of( - new RemoteClusterCredentials(remoteClusterAlias, ApiKeyService.withApiKeyPrefix(remoteClusterCredentials.toString())) - ); - } - - private void sendWithCrossClusterAccessHeaders( - final RemoteClusterCredentials remoteClusterCredentials, - final Transport.Connection connection, - final String action, - final TransportRequest request, - final TransportRequestOptions options, - final TransportResponseHandler handler - ) { - if (false == Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.check(licenseState)) { - throw LicenseUtils.newComplianceException(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE.getName()); - } - final String remoteClusterAlias = remoteClusterCredentials.clusterAlias(); - - if (connection.getTransportVersion().before(TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY)) { - throw illegalArgumentExceptionWithDebugLog( - "Settings for remote cluster [" - + remoteClusterAlias - + "] indicate cross cluster access headers should be sent but target cluster version [" - + connection.getTransportVersion().toReleaseVersion() - + "] does not support receiving them" - ); - } - - logger.trace( - () -> format( - "Sending [%s] request for [%s] action to [%s] with cross cluster access headers", - request.getClass(), - action, - remoteClusterAlias - ) - ); - - final Authentication authentication = securityContext.getAuthentication(); - assert authentication != null : "authentication must be present in security context"; - - final User user = authentication.getEffectiveSubject().getUser(); - if (user instanceof InternalUser && false == SystemUser.is(user)) { - final String message = "Internal user [" + user.principal() + "] should not be used for cross cluster requests"; - assert false : message; - throw illegalArgumentExceptionWithDebugLog(message); - } else if (SystemUser.is(user) || action.equals(ClusterStateAction.NAME)) { - if (SystemUser.is(user)) { - logger.trace( - "Request [{}] for action [{}] towards [{}] initiated by the system user. " - + "Sending request with internal cross cluster access user headers", - request.getClass(), - action, - remoteClusterAlias - ); - } else { - // Use system user for cluster state requests (CCR has many calls of cluster state with end-user context) - logger.trace( - () -> format( - "Switching to the system user for cluster state action towards [{}]. Original user is [%s]", - remoteClusterAlias, - user - ) - ); - } - final var crossClusterAccessHeaders = new CrossClusterAccessHeaders( - remoteClusterCredentials.credentials(), - SystemUser.crossClusterAccessSubjectInfo( - authentication.getEffectiveSubject().getTransportVersion(), - authentication.getEffectiveSubject().getRealm().getNodeName() - ) - ); - // To be able to enforce index-level privileges under the new remote cluster security model, - // we switch from old-style internal actions to their new equivalent indices actions so that - // they will be checked for index privileges against the index specified in the requests - final String effectiveAction = RCS_INTERNAL_ACTIONS_REPLACEMENTS.getOrDefault(action, action); - if (false == effectiveAction.equals(action)) { - logger.trace("switching internal action from [{}] to [{}]", action, effectiveAction); - } - sendWithCrossClusterAccessHeaders(crossClusterAccessHeaders, connection, effectiveAction, request, options, handler); - } else { - assert false == action.startsWith("internal:") : "internal action must be sent with system user"; - authzService.getRoleDescriptorsIntersectionForRemoteCluster( - remoteClusterAlias, - connection.getTransportVersion(), - authentication.getEffectiveSubject(), - ActionListener.wrap(roleDescriptorsIntersection -> { - logger.trace( - () -> format( - "Subject [%s] has role descriptors intersection [%s] for action [%s] towards remote cluster [%s]", - authentication.getEffectiveSubject(), - roleDescriptorsIntersection, - action, - remoteClusterAlias - ) - ); - if (roleDescriptorsIntersection.isEmpty()) { - throw authzService.remoteActionDenied( - authentication, - SecurityActionMapper.action(action, request), - remoteClusterAlias - ); - } - final var crossClusterAccessHeaders = new CrossClusterAccessHeaders( - remoteClusterCredentials.credentials(), - new CrossClusterAccessSubjectInfo(authentication, roleDescriptorsIntersection) - ); - sendWithCrossClusterAccessHeaders(crossClusterAccessHeaders, connection, action, request, options, handler); - }, // it's safe to not use a context restore handler here since `getRoleDescriptorsIntersectionForRemoteCluster` - // uses a context preserving listener internally, and `sendWithCrossClusterAccessHeaders` uses a context restore - // handler - e -> handler.handleException(new SendRequestTransportException(connection.getNode(), action, e)) - ) - ); - } - } - - private void sendWithCrossClusterAccessHeaders( - final CrossClusterAccessHeaders crossClusterAccessHeaders, - final Transport.Connection connection, - final String action, - final TransportRequest request, - final TransportRequestOptions options, - final TransportResponseHandler handler - ) { - final ThreadContext threadContext = securityContext.getThreadContext(); - final var contextRestoreHandler = new ContextRestoreResponseHandler<>(threadContext.newRestorableContext(true), handler); - try (ThreadContext.StoredContext ignored = threadContext.stashContextPreservingRequestHeaders(AuditUtil.AUDIT_REQUEST_ID)) { - crossClusterAccessHeaders.writeToContext(threadContext); - sender.sendRequest(connection, action, request, options, contextRestoreHandler); - } catch (Exception e) { - contextRestoreHandler.handleException(new SendRequestTransportException(connection.getNode(), action, e)); - } - } - - private static IllegalArgumentException illegalArgumentExceptionWithDebugLog(String message) { - logger.debug(message); - return new IllegalArgumentException(message); - } - }; - } - private void sendWithUser( Transport.Connection connection, String action, @@ -457,7 +188,8 @@ private void sendWithUser( throw new IllegalStateException("there should always be a user when sending a message for action [" + action + "]"); } - assert securityContext.getParentAuthorization() == null || remoteClusterCredentialsResolver.apply(connection).isEmpty() + assert securityContext.getParentAuthorization() == null + || false == remoteClusterTransportInterceptor.isRemoteClusterConnection(connection) : "parent authorization header should not be set for remote cluster requests"; try { @@ -482,52 +214,6 @@ public TransportRequestHandler interceptHandler( return new ProfileSecuredRequestHandler<>(logger, action, forceExecution, executor, actualHandler, profileFilters, threadPool); } - private Map initializeProfileFilters(DestructiveOperations destructiveOperations) { - final Map profileConfigurations = ProfileConfigurations.get(settings, sslService, false); - - Map profileFilters = Maps.newMapWithExpectedSize(profileConfigurations.size() + 1); - - final boolean transportSSLEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); - final boolean remoteClusterPortEnabled = REMOTE_CLUSTER_SERVER_ENABLED.get(settings); - final boolean remoteClusterServerSSLEnabled = XPackSettings.REMOTE_CLUSTER_SERVER_SSL_ENABLED.get(settings); - - for (Map.Entry entry : profileConfigurations.entrySet()) { - final String profileName = entry.getKey(); - final SslProfile sslProfile = entry.getValue(); - final SslConfiguration profileConfiguration = sslProfile.configuration(); - assert profileConfiguration != null : "Ssl Profile [" + sslProfile + "] for [" + profileName + "] has a null configuration"; - final boolean useRemoteClusterProfile = remoteClusterPortEnabled && profileName.equals(REMOTE_CLUSTER_PROFILE); - if (useRemoteClusterProfile) { - profileFilters.put( - profileName, - new CrossClusterAccessServerTransportFilter( - crossClusterAccessAuthcService, - authzService, - threadPool.getThreadContext(), - remoteClusterServerSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration), - destructiveOperations, - securityContext, - licenseState - ) - ); - } else { - profileFilters.put( - profileName, - new ServerTransportFilter( - authcService, - authzService, - threadPool.getThreadContext(), - transportSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration), - destructiveOperations, - securityContext - ) - ); - } - } - - return Collections.unmodifiableMap(profileFilters); - } - public static class ProfileSecuredRequestHandler implements TransportRequestHandler { private final String action; @@ -681,11 +367,4 @@ public void onFailure(Exception e) { } } - record RemoteClusterCredentials(String clusterAlias, String credentials) { - - @Override - public String toString() { - return "RemoteClusterCredentials{clusterAlias='" + clusterAlias + "', credentials='::es_redacted::'}"; - } - } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java similarity index 97% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java index 70e9db73c488f..6747c9648f817 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.security.crossclusteraccess; +package org.elasticsearch.xpack.security.authc; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; @@ -31,10 +31,6 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.user.InternalUsers; -import org.elasticsearch.xpack.security.authc.ApiKeyService; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessAuthenticationService; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders; -import org.elasticsearch.xpack.security.authc.CrossClusterAccessHeadersTests; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -146,7 +142,7 @@ public void testInvalidHeaders() throws IOException { } } - public void testTryAuthenticateSuccess() throws IOException { + public void testAuthenticateHeadersSuccess() throws IOException { final String encodedCrossClusterAccessApiKey = getEncodedCrossClusterAccessApiKey(); final String nodeName = internalCluster().getRandomNodeName(); final ThreadContext threadContext = internalCluster().getInstance(SecurityContext.class, nodeName).getThreadContext(); @@ -213,7 +209,7 @@ public void testGetApiKeyCredentialsFromHeaders() { } - public void testTryAuthenticateFailure() throws IOException { + public void testAuthenticateHeadersFailure() throws IOException { final EncodedKeyWithId encodedCrossClusterAccessApiKeyWithId = getEncodedCrossClusterAccessApiKeyWithId(); final EncodedKeyWithId encodedRestApiKeyWithId = getEncodedRestApiKeyWithId(); final String nodeName = internalCluster().getRandomNodeName(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java index 31c6d6f0c2341..6e71b5f04fe7b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java @@ -193,7 +193,7 @@ public void testNoInteractionWithAuditableRequestOnInitialAuthenticationFailure( verifyNoInteractions(auditableRequest); } - public void testTerminateExceptionBubblesUpWithTryAuthenticate() { + public void testTerminateExceptionBubblesUpWithAuthenticateHeaders() { @SuppressWarnings("unchecked") final ArgumentCaptor>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); doAnswer(i -> null).when(apiKeyService) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java index ff92b7a1e7dcd..c2aa1acf4d308 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java @@ -24,8 +24,6 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; -import java.util.Optional; - import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.RESTRICTED_INDICES; import static org.elasticsearch.xpack.security.authz.PreAuthorizationUtils.maybeSkipChildrenActionAuthorization; import static org.elasticsearch.xpack.security.authz.PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext; @@ -71,9 +69,9 @@ public void testShouldRemoveParentAuthorizationFromThreadContext() { // We should not remove the parent authorization when child action is white-listed assertThat( shouldRemoveParentAuthorizationFromThreadContext( - Optional.empty(), randomWhitelistedChildAction(parentAction), - securityContextWithParentAuthorization + securityContextWithParentAuthorization, + false ), equalTo(false) ); @@ -81,9 +79,9 @@ public void testShouldRemoveParentAuthorizationFromThreadContext() { // We should not remove when there is nothing to be removed assertThat( shouldRemoveParentAuthorizationFromThreadContext( - Optional.ofNullable(randomBoolean() ? "my_remote_cluster" : null), randomWhitelistedChildAction(parentAction), - new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)) + new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)), + randomBoolean() ), equalTo(false) ); @@ -92,9 +90,9 @@ public void testShouldRemoveParentAuthorizationFromThreadContext() { // we expect to remove parent authorization when targeting remote cluster assertThat( shouldRemoveParentAuthorizationFromThreadContext( - Optional.of("my_remote_cluster"), randomWhitelistedChildAction(parentAction), - securityContextWithParentAuthorization + securityContextWithParentAuthorization, + true ), equalTo(true) ); @@ -104,9 +102,9 @@ public void testShouldRemoveParentAuthorizationFromThreadContext() { // - or the child action is not white-listed for the parent assertThat( shouldRemoveParentAuthorizationFromThreadContext( - Optional.ofNullable(randomBoolean() ? "my_remote_cluster" : null), randomAlphaOfLengthBetween(3, 8), - securityContextWithParentAuthorization + securityContextWithParentAuthorization, + randomBoolean() ), equalTo(true) ); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index 655e4eba4a179..54ef194fba430 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -120,6 +120,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { private SecurityContext securityContext; private ClusterService clusterService; private MockLicenseState mockLicenseState; + private DestructiveOperations destructiveOperations; @Override public void setUp() throws Exception { @@ -131,6 +132,10 @@ public void setUp() throws Exception { securityContext = spy(new SecurityContext(settings, threadPool.getThreadContext())); mockLicenseState = MockLicenseState.createMock(); Mockito.when(mockLicenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(true); + destructiveOperations = new DestructiveOperations( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) + ); } @After @@ -149,16 +154,18 @@ public void testSendAsync() throws Exception { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -200,16 +207,18 @@ public void testSendAsyncSwitchToSystem() throws Exception { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -244,16 +253,18 @@ public void testSendWithoutUser() throws Exception { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ) { @Override void assertNoAuthentication(String action) {} @@ -306,16 +317,18 @@ public void testSendToNewerVersionSetsCorrectVersion() throws Exception { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -374,16 +387,18 @@ public void testSendToOlderVersionSetsCorrectVersion() throws Exception { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -440,16 +455,18 @@ public void testSetUserBasedOnActionOrigin() { SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); @@ -607,17 +624,19 @@ public void testSendWithCrossClusterAccessHeadersWithUnsupportedLicense() throws final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - unsupportedLicenseState, - mockRemoteClusterCredentialsResolver(remoteClusterAlias) + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + unsupportedLicenseState, + mockRemoteClusterCredentialsResolver(remoteClusterAlias) + ) ); final AsyncSender sender = interceptor.interceptSender(mock(AsyncSender.class, ignored -> { @@ -744,17 +763,21 @@ private void doTestSendWithCrossClusterAccessHeaders( final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - authzService, mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + authzService, + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState, + ignored -> Optional.of( + new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray())) + ) + ) ); final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); @@ -882,21 +905,25 @@ public void testSendWithUserIfCrossClusterAccessHeadersConditionNotMet() throws final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - authzService, mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - ignored -> notRemoteConnection - ? Optional.empty() - : (finalNoCredential - ? Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, null)) - : Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray())))) + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + authzService, + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState, + ignored -> notRemoteConnection + ? Optional.empty() + : (finalNoCredential + ? Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, null)) + : Optional.of( + new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray())) + )) + ) ); final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); @@ -941,17 +968,21 @@ public void testSendWithCrossClusterAccessHeadersThrowsOnOldConnection() throws final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState, + ignored -> Optional.of( + new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray())) + ) + ) ); final AsyncSender sender = interceptor.interceptSender(new AsyncSender() { @@ -1040,17 +1071,21 @@ public void testSendRemoteRequestFailsIfUserHasNoRemoteIndicesPrivileges() throw final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( settings, threadPool, - mock(AuthenticationService.class), - authzService, mockSslService(), securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + authzService, + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState, + ignored -> Optional.of( + new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray())) + ) + ) ); final AsyncSender sender = interceptor.interceptSender(new AsyncSender() { @@ -1150,16 +1185,18 @@ public void testProfileFiltersCreatedDifferentlyForDifferentTransportAndRemoteCl final var securityServerTransportInterceptor = new SecurityServerTransportInterceptor( builder.build(), threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), sslService, securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + builder.build(), + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); @@ -1208,16 +1245,18 @@ public void testNoProfileFilterForRemoteClusterWhenTheFeatureIsDisabled() { final var securityServerTransportInterceptor = new SecurityServerTransportInterceptor( builder.build(), threadPool, - mock(AuthenticationService.class), - mock(AuthorizationService.class), sslService, securityContext, - new DestructiveOperations( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) - ), - mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + destructiveOperations, + new CrossClusterAccessTransportInterceptor( + builder.build(), + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + securityContext, + mock(CrossClusterAccessAuthenticationService.class), + mockLicenseState + ) ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4ServerTransportAuthenticationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4ServerTransportAuthenticationTests.java index 26a8d62c66f93..bb725690a6fa0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4ServerTransportAuthenticationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4ServerTransportAuthenticationTests.java @@ -115,7 +115,7 @@ public void setUp() throws Exception { ((ActionListener) invocation.getArguments()[1]).onResponse(null); } return null; - }).when(remoteCrossClusterAccessAuthenticationService).tryAuthenticate(any(Map.class), anyActionListener()); + }).when(remoteCrossClusterAccessAuthenticationService).authenticateHeaders(any(Map.class), anyActionListener()); remoteSecurityNetty4ServerTransport = new SecurityNetty4ServerTransport( remoteSettings, TransportVersion.current(),