Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.search.internal.ShardSearchRequest;
import org.elasticsearch.telemetry.TelemetryProvider;
import org.elasticsearch.threadpool.ExecutorBuilder;
Expand Down Expand Up @@ -1162,7 +1163,8 @@ Collection<Object> createComponents(
authorizationDenialMessages.get(),
linkedProjectConfigService,
projectResolver,
getCustomAuthorizedProjectsResolverOrDefault(extensionComponents)
getCustomAuthorizedProjectsResolverOrDefault(extensionComponents),
new CrossProjectModeDecider(settings)
);

components.add(nativeRolesStore); // used by roles actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ public AuthorizationService(
AuthorizationDenialMessages authorizationDenialMessages,
LinkedProjectConfigService linkedProjectConfigService,
ProjectResolver projectResolver,
AuthorizedProjectsResolver authorizedProjectsResolver
AuthorizedProjectsResolver authorizedProjectsResolver,
CrossProjectModeDecider crossProjectModeDecider
Copy link
Contributor Author

@slobodanadamovic slobodanadamovic Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Providing CrossProjectModeDecider via constructor to allow easier testing of project authorization code-paths.

) {
this.clusterService = clusterService;
this.auditTrailService = auditTrailService;
Expand All @@ -181,7 +182,7 @@ public AuthorizationService(
settings,
linkedProjectConfigService,
resolver,
new CrossProjectModeDecider(settings)
crossProjectModeDecider
);
this.authcFailureHandler = authcFailureHandler;
this.threadContext = threadPool.getThreadContext();
Expand Down Expand Up @@ -500,82 +501,102 @@ private void authorizeAction(
} else if (isIndexAction(action)) {
final ProjectMetadata projectMetadata = projectResolver.getProjectMetadata(clusterService.state());
assert projectMetadata != null;
final AsyncSupplier<ResolvedIndices> resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>(() -> {
if (request instanceof SearchRequest searchRequest && searchRequest.pointInTimeBuilder() != null) {
var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest);
return SubscribableListener.newSucceeded(resolvedIndices);
}
final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request);
if (resolvedIndices != null) {
return SubscribableListener.newSucceeded(resolvedIndices);
} else {
final SubscribableListener<ResolvedIndices> resolvedIndicesListener = new SubscribableListener<>();
final var authorizedIndicesListener = new SubscribableListener<AuthorizationEngine.AuthorizedIndices>();
authorizedIndicesListener.<Tuple<AuthorizationEngine.AuthorizedIndices, TargetProjects>>andThen(
(l, authorizedIndices) -> {
if (indicesAndAliasesResolver.resolvesCrossProject(request)) {
authorizedProjectsResolver.resolveAuthorizedProjects(
l.map(targetProjects -> new Tuple<>(authorizedIndices, targetProjects))
);
} else {
l.onResponse(new Tuple<>(authorizedIndices, TargetProjects.NOT_CROSS_PROJECT));
}
}
)
final SubscribableListener<TargetProjects> targetProjectListener;
if (indicesAndAliasesResolver.resolvesCrossProject(request)) {
targetProjectListener = new SubscribableListener<>();
authorizedProjectsResolver.resolveAuthorizedProjects(targetProjectListener);
} else {
targetProjectListener = SubscribableListener.newSucceeded(TargetProjects.NOT_CROSS_PROJECT);
}

targetProjectListener.addListener(ActionListener.wrap(targetProjects -> {
final AsyncSupplier<ResolvedIndices> resolvedIndicesAsyncSupplier = makeResolvedIndicesAsyncSupplier(
targetProjects,
requestInfo,
requestId,
request,
action,
projectMetadata,
authzInfo,
authzEngine,
auditTrail,
listener
);

// Wrapping here in order to have exceptions thrown from the {@code authorizeIndexAction} method
// get handled directly by the listener and not go through {@code onAuthorizedResourceLoadFailure}
// which wraps them in security exception. This is in order to maintain the same behavior as before.
ActionListener.run(
listener,
l -> authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier, projectMetadata)
.addListener(
ActionListener.wrap(
authorizedIndicesAndProjects -> resolvedIndicesListener.onResponse(
indicesAndAliasesResolver.resolve(
action,
request,
wrapPreservingContext(
new AuthorizationResultListener<>(
result -> handleIndexActionAuthorizationResult(
result,
requestInfo,
requestId,
authzInfo,
authzEngine,
resolvedIndicesAsyncSupplier,
projectMetadata,
authorizedIndicesAndProjects.v1(),
authorizedIndicesAndProjects.v2()
)
l
),
l::onFailure,
requestInfo,
requestId,
authzInfo
),
e -> onAuthorizedResourceLoadFailure(requestId, requestInfo, authzInfo, auditTrail, listener, e)
threadContext
)
);

authzEngine.loadAuthorizedIndices(
requestInfo,
authzInfo,
projectMetadata.getIndicesLookup(),
authorizedIndicesListener
);

return resolvedIndicesListener;
}
});
authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier, projectMetadata)
.addListener(
wrapPreservingContext(
new AuthorizationResultListener<>(
result -> handleIndexActionAuthorizationResult(
result,
requestInfo,
requestId,
authzInfo,
authzEngine,
resolvedIndicesAsyncSupplier,
projectMetadata,
listener
),
listener::onFailure,
requestInfo,
requestId,
authzInfo
),
threadContext
)
)
);
}, e -> onAuthorizedResourceLoadFailure(requestId, requestInfo, authzInfo, auditTrail, listener, e)));
} else {
logger.warn("denying access for [{}] as action [{}] is not an index or cluster action", authentication, action);
auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
listener.onFailure(actionDenied(authentication, authzInfo, action, request));
}
}

private AsyncSupplier<ResolvedIndices> makeResolvedIndicesAsyncSupplier(
TargetProjects targetProjects,
RequestInfo requestInfo,
String requestId,
TransportRequest request,
String action,
ProjectMetadata projectMetadata,
AuthorizationInfo authzInfo,
AuthorizationEngine authzEngine,
AuditTrail auditTrail,
ActionListener<Void> listener
) {
return new CachingAsyncSupplier<>(() -> {
if (request instanceof SearchRequest searchRequest && searchRequest.pointInTimeBuilder() != null) {
var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest);
return SubscribableListener.newSucceeded(resolvedIndices);
}
final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request);
if (resolvedIndices != null) {
return SubscribableListener.newSucceeded(resolvedIndices);
} else {
final SubscribableListener<ResolvedIndices> resolvedIndicesListener = new SubscribableListener<>();
authzEngine.loadAuthorizedIndices(
requestInfo,
authzInfo,
projectMetadata.getIndicesLookup(),
ActionListener.wrap(authorizedIndices -> {
resolvedIndicesListener.onResponse(
indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices, targetProjects)
);
}, e -> onAuthorizedResourceLoadFailure(requestId, requestInfo, authzInfo, auditTrail, listener, e))
);

return resolvedIndicesListener;
}
});
}

private void onAuthorizedResourceLoadFailure(
String requestId,
RequestInfo requestInfo,
Expand Down
Loading