diff --git a/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java new file mode 100644 index 0000000000000..2d4594c05c6f1 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch; + +import org.elasticsearch.action.IndicesRequest; + +import java.util.List; + +public interface FlatIndicesRequest extends IndicesRequest { + boolean requiresRewrite(); + + void indexExpressions(List indexExpressions); + + record IndexExpression(String original, List rewritten) {} +} diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index fda2df81d3f94..cd5c0ad466bc7 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -9,6 +9,7 @@ package org.elasticsearch.action.search; +import org.elasticsearch.FlatIndicesRequest; import org.elasticsearch.TransportVersions; import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; @@ -53,7 +54,11 @@ * @see Client#search(SearchRequest) * @see SearchResponse */ -public class SearchRequest extends LegacyActionRequest implements IndicesRequest.Replaceable, Rewriteable { +public class SearchRequest extends LegacyActionRequest + implements + FlatIndicesRequest, + IndicesRequest.Replaceable, + Rewriteable { public static final ToXContent.Params FORMAT_PARAMS = new ToXContent.MapParams(Collections.singletonMap("pretty", "false")); @@ -70,6 +75,9 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest private String[] indices = Strings.EMPTY_ARRAY; + @Nullable + private List indexExpressions; + @Nullable private String routing; @Nullable @@ -853,4 +861,16 @@ public String toString() { + source + '}'; } + + @Override + public boolean requiresRewrite() { + return indexExpressions == null; + } + + @Override + public void indexExpressions(List indexExpressions) { + assert requiresRewrite(); + this.indexExpressions = indexExpressions; + indices(indexExpressions.stream().flatMap(indexExpression -> indexExpression.rewritten().stream()).toArray(String[]::new)); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 2c831645d0e69..e2ffcf7480381 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -299,6 +299,11 @@ interface AuthorizedIndices { * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ boolean check(String name, IndexComponentSelector selector); + + // Does not belong here + default boolean checkProject(String projectId) { + return false; + } } /** 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..e895c8be3ce69 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 @@ -6,6 +6,9 @@ */ package org.elasticsearch.xpack.security.authz; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.FlatIndicesRequest; import org.elasticsearch.action.AliasesRequest; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; @@ -55,6 +58,8 @@ class IndicesAndAliasesResolver { + private static final Logger logger = LogManager.getLogger(IndicesAndAliasesResolver.class); + private final IndexNameExpressionResolver nameExpressionResolver; private final IndexAbstractionResolver indexAbstractionResolver; private final RemoteClusterResolver remoteClusterResolver; @@ -103,7 +108,6 @@ class IndicesAndAliasesResolver { * resolving wildcards. *

*/ - ResolvedIndices resolve( String action, TransportRequest request, @@ -124,9 +128,62 @@ ResolvedIndices resolve( if (request instanceof IndicesRequest == false) { throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be."); } + + if (request instanceof FlatIndicesRequest flatIndicesRequest && flatIndicesRequest.requiresRewrite()) { + rewrite(flatIndicesRequest, authorizedIndices); + } + return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices); } + void rewrite(FlatIndicesRequest request, AuthorizationEngine.AuthorizedIndices authorizedIndices) { + assert request.requiresRewrite(); + + var clusters = remoteClusterResolver.clusters(); + logger.info("Clusters available for remote indices: {}", clusters); + // no remotes, nothing to rewrite... + if (clusters.isEmpty()) { + logger.info("No remote clusters linked, skipping..."); + return; + } + + var indices = request.indices(); + // empty indices actually means search everything so would need to also rewrite that + + var authorizedClusters = new HashSet(); + for (var cluster : clusters) { + if (authorizedIndices.checkProject(cluster)) { + logger.info("Remote cluster [{}] authorized", cluster); + authorizedClusters.add(cluster); + } + } + + if (authorizedClusters.isEmpty()) { + logger.info("No remote clusters authorized, skipping..."); + return; + } + + ResolvedIndices resolved = remoteClusterResolver.splitLocalAndRemoteIndexNames(indices); + // skip handling searches where there's both qualified and flat expressions to simplify POC + // in the real thing, we'd also rewrite these + if (resolved.getRemote().isEmpty() == false) { + return; + } + + List indexExpressions = new ArrayList<>(indices.length); + for (var local : resolved.getLocal()) { + List rewritten = new ArrayList<>(); + rewritten.add(local); + for (var cluster : authorizedClusters) { + rewritten.add(RemoteClusterAware.buildRemoteIndexName(cluster, local)); + indexExpressions.add(new FlatIndicesRequest.IndexExpression(local, rewritten)); + } + logger.info("Rewrote [{}] to [{}]", local, rewritten); + } + + request.indexExpressions(indexExpressions); + } + /** * Attempt to resolve requested indices without expanding any wildcards. * @return The {@link ResolvedIndices} or null if wildcard expansion must be performed. @@ -569,5 +626,9 @@ ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) { .toList(); return new ResolvedIndices(local == null ? List.of() : local, remote); } + + Set clusters() { + return Collections.unmodifiableSet(clusters); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 1b99bd6888c4f..9c6b8c26ae312 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -998,6 +998,10 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } // we don't support granting access to a backing index with a failure selector via the parent data stream } return predicate.test(indexAbstraction, selector); + }, name -> { + // just some bogus predicate that lets us differentiate between roles, not at all + // how this will work in the end + return Arrays.asList(role.names()).contains("_es_test_root"); }); } @@ -1125,15 +1129,18 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn private final CachedSupplier> authorizedAndAvailableDataResources; private final CachedSupplier> authorizedAndAvailableFailuresResources; private final BiPredicate isAuthorizedPredicate; + private final Predicate projectPredicate; AuthorizedIndices( Supplier> authorizedAndAvailableDataResources, Supplier> authorizedAndAvailableFailuresResources, - BiPredicate isAuthorizedPredicate + BiPredicate isAuthorizedPredicate, + Predicate projectPredicate ) { this.authorizedAndAvailableDataResources = CachedSupplier.wrap(authorizedAndAvailableDataResources); this.authorizedAndAvailableFailuresResources = CachedSupplier.wrap(authorizedAndAvailableFailuresResources); this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate); + this.projectPredicate = projectPredicate; } @Override @@ -1149,5 +1156,11 @@ public boolean check(String name, IndexComponentSelector selector) { Objects.requireNonNull(selector, "must specify a selector for authorization check"); return isAuthorizedPredicate.test(name, selector); } + + @Override + public boolean checkProject(String name) { + Objects.requireNonNull(name, "must specify a project name for authorization check"); + return projectPredicate.test(name); + } } }