diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index ec30886b1acbf..bc01b5daa8f9a 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoostingQueryBuilder; import org.elasticsearch.index.query.ConstantScoreQueryBuilder; @@ -46,6 +47,9 @@ public final class FieldCapabilitiesRequest extends LegacyActionRequest implemen private String clusterAlias = RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY; + @Nullable + private String projectRouting; + private String[] indices = Strings.EMPTY_ARRAY; private IndicesOptions indicesOptions = DEFAULT_INDICES_OPTIONS; private String[] fields = Strings.EMPTY_ARRAY; @@ -113,6 +117,15 @@ String clusterAlias() { return clusterAlias; } + @Nullable + public String projectRouting() { + return projectRouting; + } + + public void projectRouting(String projectRouting) { + this.projectRouting = projectRouting; + } + @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java index d72f3ef0529ad..ea84fc27e85a0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java @@ -270,6 +270,8 @@ private static FieldCapabilitiesRequest createFieldCapsRequest(String index, Set req.fields(fieldNames.toArray(String[]::new)); req.includeUnmapped(true); req.indexFilter(requestFilter); + // Note: this is just some bogus query to demonstrate that we can invoke the full ES|QL engine from the security layer + req.projectRouting("row a = 1, b = \"x\", c = 1000000000000, d = 1.1"); // lenient because we throw our own errors looking at the response e.g. if something was not resolved // also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable req.indicesOptions(FIELD_CAPS_INDICES_OPTIONS); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/EsqlProjectRouter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/EsqlProjectRouter.java new file mode 100644 index 0000000000000..42a04a228cfab --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/EsqlProjectRouter.java @@ -0,0 +1,60 @@ +/* + * 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; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.xpack.core.esql.action.EsqlQueryRequest; +import org.elasticsearch.xpack.core.esql.action.EsqlQueryRequestBuilder; +import org.elasticsearch.xpack.core.esql.action.EsqlQueryResponse; + +import java.util.List; + +public class EsqlProjectRouter { + + private static final Logger logger = LogManager.getLogger(EsqlProjectRouter.class); + + private final Client client; + + public EsqlProjectRouter(Client client) { + this.client = client; + } + + public List route(List projects, String projectRoutingQuery) { + // Note: this just demonstrates that we can invoke the full ES|QL engine from the security layer + // we certainly don't want to just run a generic ES|QL query + + // Instead we should expose the EsqlProjectRoutingAction to be called the same way we + // expose the EsqlQueryAction and call EsqlProjectRoutingAction here + // (we will need to repeat the dance done in https://github.com/elastic/elasticsearch/issues/104413) + + // EsqlProjectRoutingAction will be localOnly as it will only access an on-the fly in-memory index + // so there are no network costs associated + + @SuppressWarnings("unchecked") + EsqlQueryRequestBuilder b = (EsqlQueryRequestBuilder< + EsqlQueryRequest, + EsqlQueryResponse>) EsqlQueryRequestBuilder.newRequestBuilder(client); + + b.query(projectRoutingQuery).execute(new ActionListener<>() { + @Override + public void onResponse(EsqlQueryResponse response) { + logger.info("EsqlProjectRouter response: {}", response); + } + + @Override + public void onFailure(Exception e) { + logger.warn("EsqlProjectRouter failure", e); + } + }); + + return projects; + } +} 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 a82200aadac2d..5e9b3ce3cc956 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 @@ -1130,7 +1130,8 @@ Collection createComponents( operatorPrivilegesService.get(), restrictedIndices, authorizationDenialMessages.get(), - projectResolver + projectResolver, + new EsqlProjectRouter(client) ); components.add(nativeRolesStore); // used by roles actions diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index f7f0f48f1c0fe..56c9004f20636 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -81,6 +81,7 @@ 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.security.EsqlProjectRouter; import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; @@ -164,12 +165,13 @@ public AuthorizationService( OperatorPrivilegesService operatorPrivilegesService, RestrictedIndices restrictedIndices, AuthorizationDenialMessages authorizationDenialMessages, - ProjectResolver projectResolver + ProjectResolver projectResolver, + EsqlProjectRouter router ) { this.clusterService = clusterService; this.auditTrailService = auditTrailService; this.restrictedIndices = restrictedIndices; - this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver); + this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver, router); this.authcFailureHandler = authcFailureHandler; this.threadContext = threadPool.getThreadContext(); this.securityContext = new SecurityContext(settings, this.threadContext); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index ff39fd587dc3a..6918642b2606a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; import org.elasticsearch.action.search.SearchContextId; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.IndexComponentSelector; @@ -38,6 +39,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.security.EsqlProjectRouter; import java.util.ArrayList; import java.util.Arrays; @@ -58,11 +60,19 @@ class IndicesAndAliasesResolver { private final IndexNameExpressionResolver nameExpressionResolver; private final IndexAbstractionResolver indexAbstractionResolver; private final RemoteClusterResolver remoteClusterResolver; + private final EsqlProjectRouter router; - IndicesAndAliasesResolver(Settings settings, ClusterService clusterService, IndexNameExpressionResolver resolver) { + IndicesAndAliasesResolver( + Settings settings, + ClusterService clusterService, + IndexNameExpressionResolver resolver, + EsqlProjectRouter router + ) { this.nameExpressionResolver = resolver; this.indexAbstractionResolver = new IndexAbstractionResolver(resolver); this.remoteClusterResolver = new RemoteClusterResolver(settings, clusterService.getClusterSettings()); + this.router = router; + } /** @@ -124,6 +134,13 @@ ResolvedIndices resolve( if (request instanceof IndicesRequest == false) { throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be."); } + + if (request instanceof FieldCapabilitiesRequest fieldCapabilitiesRequest) { + if (fieldCapabilitiesRequest.projectRouting() != null) { + router.route(List.of("project1", "project2", "project3"), fieldCapabilitiesRequest.projectRouting()); + } + } + return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index e4bb33c66d983..18340640814ad 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -336,7 +336,8 @@ public void setup() { operatorPrivilegesService, RESTRICTED_INDICES, new AuthorizationDenialMessages.Default(), - projectResolver + projectResolver, + null ); } @@ -1769,7 +1770,8 @@ public void testDenialForAnonymousUser() { operatorPrivilegesService, RESTRICTED_INDICES, new AuthorizationDenialMessages.Default(), - projectResolver + projectResolver, + null ); RoleDescriptor role = new RoleDescriptor( @@ -1819,7 +1821,8 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { operatorPrivilegesService, RESTRICTED_INDICES, new AuthorizationDenialMessages.Default(), - projectResolver + projectResolver, + null ); RoleDescriptor role = new RoleDescriptor( @@ -3357,7 +3360,8 @@ public void testAuthorizationEngineSelectionForCheckPrivileges() throws Exceptio operatorPrivilegesService, RESTRICTED_INDICES, new AuthorizationDenialMessages.Default(), - projectResolver + projectResolver, + null ); Subject subject = new Subject(new User("test", "a role"), mock(RealmRef.class)); @@ -3513,7 +3517,8 @@ public void getUserPrivileges(AuthorizationInfo authorizationInfo, ActionListene operatorPrivilegesService, RESTRICTED_INDICES, new AuthorizationDenialMessages.Default(), - projectResolver + projectResolver, + null ); Authentication authentication; try (StoredContext ignore = threadContext.stashContext()) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 6ca47dd9807e1..9e02136057fc3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -418,7 +418,7 @@ public void setup() { ClusterService clusterService = mock(ClusterService.class); when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); - defaultIndicesResolver = new IndicesAndAliasesResolver(settings, clusterService, indexNameExpressionResolver); + defaultIndicesResolver = new IndicesAndAliasesResolver(settings, clusterService, indexNameExpressionResolver, null); } public void testDashIndicesAreAllowedInShardLevelRequests() {