diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlUpgradeModeActionFilterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlUpgradeModeActionFilterTests.java index 7ecf98cd7a6dd..3092808dc91f8 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlUpgradeModeActionFilterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlUpgradeModeActionFilterTests.java @@ -31,6 +31,8 @@ import org.junit.After; import org.junit.Before; +import java.util.Set; + import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -114,7 +116,7 @@ public void testApply_ActionDisallowedInUpgradeModeWithResetModeExemption() { public void testOrder_UpgradeFilterIsExecutedAfterSecurityFilter() { MlUpgradeModeActionFilter upgradeModeFilter = new MlUpgradeModeActionFilter(clusterService); - SecurityActionFilter securityFilter = new SecurityActionFilter(null, null, null, null, mock(ThreadPool.class), null, null); + SecurityActionFilter securityFilter = new SecurityActionFilter(null, null, null, null, mock(ThreadPool.class), null, null, Set::of); ActionFilter[] actionFiltersInOrderOfExecution = new ActionFilters(Sets.newHashSet(upgradeModeFilter, securityFilter)).filters(); assertThat(actionFiltersInOrderOfExecution, is(arrayContaining(securityFilter, upgradeModeFilter))); 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 ae6df838b4eac..05454ae5429db 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 @@ -288,6 +288,7 @@ import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountTokenStore; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.support.SecondaryAuthActions; import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.authz.AuthorizationDenialMessages; @@ -583,6 +584,7 @@ public class Security extends Plugin private final SetOnce client = new SetOnce<>(); private final SetOnce> reloadableComponents = new SetOnce<>(); private final SetOnce authorizationDenialMessages = new SetOnce<>(); + private final SetOnce secondaryAuthActions = new SetOnce<>(); public Security(Settings settings) { this(settings, Collections.emptyList()); @@ -1071,7 +1073,8 @@ Collection createComponents( getLicenseState(), threadPool, securityContext.get(), - destructiveOperations + destructiveOperations, + secondaryAuthActions.get() == null ? Set::of : secondaryAuthActions.get() ) ); @@ -2105,6 +2108,7 @@ public void loadExtensions(ExtensionLoader loader) { loadSingletonExtensionAndSetOnce(loader, createApiKeyRequestBuilderFactory, CreateApiKeyRequestBuilderFactory.class); loadSingletonExtensionAndSetOnce(loader, hasPrivilegesRequestBuilderFactory, HasPrivilegesRequestBuilderFactory.class); loadSingletonExtensionAndSetOnce(loader, authorizationDenialMessages, AuthorizationDenialMessages.class); + loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class); // TODO: move this xpack core } private void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce setOnce, Class clazz) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java index 08544d316e87a..399e2572031f1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java @@ -28,12 +28,14 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication; import org.elasticsearch.xpack.core.security.authz.privilege.HealthAndStatsPrivilege; import org.elasticsearch.xpack.core.security.user.InternalUsers; import org.elasticsearch.xpack.security.action.SecurityActionMapper; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.support.SecondaryAuthActions; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationUtils; @@ -51,6 +53,7 @@ public class SecurityActionFilter implements ActionFilter { private final ThreadContext threadContext; private final SecurityContext securityContext; private final DestructiveOperations destructiveOperations; + private final SecondaryAuthActions secondaryAuthActions; public SecurityActionFilter( AuthenticationService authcService, @@ -59,7 +62,8 @@ public SecurityActionFilter( XPackLicenseState licenseState, ThreadPool threadPool, SecurityContext securityContext, - DestructiveOperations destructiveOperations + DestructiveOperations destructiveOperations, + SecondaryAuthActions secondaryAuthActions ) { this.authcService = authcService; this.authzService = authzService; @@ -68,6 +72,7 @@ public SecurityActionFilter( this.threadContext = threadPool.getThreadContext(); this.securityContext = securityContext; this.destructiveOperations = destructiveOperations; + this.secondaryAuthActions = secondaryAuthActions; } @Override @@ -109,6 +114,16 @@ operations are blocked on license expiration. All data operations (read and writ TransportVersion.current(), // current version since this is on the same node (original) -> { applyInternal(task, chain, action, request, contextPreservingListener); } ); + } else if (secondaryAuthActions.get().contains(action)) { + SecondaryAuthentication secondaryAuth = securityContext.getSecondaryAuthentication(); + if (secondaryAuth == null) { + throw new IllegalArgumentException("es-secondary-authorization header must be used to call action [" + action + "]"); + } else { + secondaryAuth.execute(ignore -> { + applyInternal(task, chain, action, request, contextPreservingListener); + return null; + }); + } } else { try (ThreadContext.StoredContext ignore = threadContext.newStoredContextPreservingResponseHeaders()) { applyInternal(task, chain, action, request, contextPreservingListener); @@ -154,6 +169,7 @@ it to the action without an associated user (not via REST or transport - this is here if a request is not associated with any other user. */ final String securityAction = SecurityActionMapper.action(action, request); + authcService.authenticate(securityAction, request, InternalUsers.SYSTEM_USER, listener.delegateFailureAndWrap((delegate, authc) -> { if (authc != null) { final String requestId = AuditUtil.extractRequestId(threadContext); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthActions.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthActions.java new file mode 100644 index 0000000000000..0c6f6e7270627 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthActions.java @@ -0,0 +1,22 @@ +/* + * 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.support; + +import java.util.Set; + +/** + * Actions that are only available when a secondary authenticator is present. The user represented by the secondary authenticator will + * be used as the user for these actions. Secondary authorization requires both the primary and secondary authentication passes. + * Any actions returned here will ensure that the RBAC authorization represents the secondary user. + * If these actions are called without a secondary authenticated user, an exception will be thrown. + * {@see SecondaryAuthenticator} + */ +@FunctionalInterface +public interface SecondaryAuthActions { + Set get(); +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java index a2ab6c1864783..586c5954a1fbb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java @@ -49,6 +49,7 @@ import org.junit.Before; import java.util.Collections; +import java.util.Set; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_KEY; @@ -114,7 +115,8 @@ public void init() throws Exception { licenseState, threadPool, securityContext, - destructiveOperations + destructiveOperations, + Set::of ); }