diff --git a/docs/changelog/134835.yaml b/docs/changelog/134835.yaml new file mode 100644 index 0000000000000..1f1b08d07a33c --- /dev/null +++ b/docs/changelog/134835.yaml @@ -0,0 +1,5 @@ +pr: 134835 +summary: Add new `/_security/stats` endpoint +area: Authorization +type: enhancement +issues: [] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/security.get_stats.json b/rest-api-spec/src/main/resources/rest-api-spec/api/security.get_stats.json new file mode 100644 index 0000000000000..6162d71f3019a --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/security.get_stats.json @@ -0,0 +1,29 @@ +{ + "security.get_stats": { + "documentation": { + "url": "https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-get-stats", + "description": "Get security statistics for all nodes" + }, + "stability": "stable", + "visibility": "public", + "headers": { + "accept": [ + "application/json" + ], + "content_type": [ + "application/json" + ] + }, + "url": { + "paths": [ + { + "path": "/_security/stats", + "methods": [ + "GET" + ] + } + ] + }, + "params": {} + } +} diff --git a/server/src/main/resources/transport/definitions/referable/security_stats_endpoint.csv b/server/src/main/resources/transport/definitions/referable/security_stats_endpoint.csv new file mode 100644 index 0000000000000..40081d05c7097 --- /dev/null +++ b/server/src/main/resources/transport/definitions/referable/security_stats_endpoint.csv @@ -0,0 +1 @@ +9168000 diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv index bf1a90e5be4e9..6e7d51d3d3020 100644 --- a/server/src/main/resources/transport/upper_bounds/9.2.csv +++ b/server/src/main/resources/transport/upper_bounds/9.2.csv @@ -1 +1 @@ -index_request_include_tsid,9167000 +security_stats_endpoint,9168000 diff --git a/x-pack/plugin/core/src/main/java/module-info.java b/x-pack/plugin/core/src/main/java/module-info.java index 9bc6377fbbd7a..132d63ac26f37 100644 --- a/x-pack/plugin/core/src/main/java/module-info.java +++ b/x-pack/plugin/core/src/main/java/module-info.java @@ -155,6 +155,7 @@ exports org.elasticsearch.xpack.core.security.action.token; exports org.elasticsearch.xpack.core.security.action.user; exports org.elasticsearch.xpack.core.security.action.settings; + exports org.elasticsearch.xpack.core.security.action.stats; exports org.elasticsearch.xpack.core.security.action; exports org.elasticsearch.xpack.core.security.authc.esnative; exports org.elasticsearch.xpack.core.security.authc.file; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsAction.java new file mode 100644 index 0000000000000..127e968629759 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsAction.java @@ -0,0 +1,19 @@ +/* + * 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.core.security.action.stats; + +import org.elasticsearch.action.ActionType; + +public class GetSecurityStatsAction extends ActionType { + + public static final GetSecurityStatsAction INSTANCE = new GetSecurityStatsAction(); + public static final String NAME = "cluster:monitor/xpack/security/stats"; + + private GetSecurityStatsAction() { + super(NAME); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeRequest.java new file mode 100644 index 0000000000000..19ed2331531a0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeRequest.java @@ -0,0 +1,34 @@ +/* + * 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.core.security.action.stats; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.transport.AbstractTransportRequest; + +import java.io.IOException; + +public class GetSecurityStatsNodeRequest extends AbstractTransportRequest { + + private static final TransportVersion SECURITY_STATS_ENDPOINT = TransportVersion.fromName("security_stats_endpoint"); + + public GetSecurityStatsNodeRequest() {} + + public GetSecurityStatsNodeRequest(final StreamInput in) throws IOException { + super(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + if (out.getTransportVersion().supports(SECURITY_STATS_ENDPOINT) == false) { // shouldn't happen, blocked at RestAction + throw new UnsupportedOperationException("node doesn't support security stats endpoint"); + } + super.writeTo(out); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeResponse.java new file mode 100644 index 0000000000000..7956ec2eabf87 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodeResponse.java @@ -0,0 +1,38 @@ +/* + * 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.core.security.action.stats; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +public class GetSecurityStatsNodeResponse extends BaseNodeResponse implements ToXContentObject { + + public GetSecurityStatsNodeResponse(final StreamInput in) throws IOException { + super(in); + } + + public GetSecurityStatsNodeResponse(final DiscoveryNode node) { + super(node); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + return builder; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesRequest.java new file mode 100644 index 0000000000000..f206f1e34241a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesRequest.java @@ -0,0 +1,16 @@ +/* + * 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.core.security.action.stats; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; + +public class GetSecurityStatsNodesRequest extends BaseNodesRequest { + public GetSecurityStatsNodesRequest() { + super((String[]) null); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesResponse.java new file mode 100644 index 0000000000000..fcb7157c52d9c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/stats/GetSecurityStatsNodesResponse.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.core.security.action.stats; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; + +public class GetSecurityStatsNodesResponse extends BaseNodesResponse implements ToXContentObject { + + public GetSecurityStatsNodesResponse( + final ClusterName clusterName, + final List nodes, + final List failures + ) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(final StreamInput in) throws IOException { + return in.readCollectionAsList(GetSecurityStatsNodeResponse::new); + } + + @Override + protected void writeNodesTo(final StreamOutput out, final List nodes) throws IOException { + out.writeCollection(nodes); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.startObject("nodes"); + for (GetSecurityStatsNodeResponse response : getNodes()) { + builder.startObject(response.getNode().getId()); + response.toXContent(builder, params); + builder.endObject(); + } + builder.endObject(); + builder.endObject(); + return builder; + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index f2634a19d1068..d992f1b028a3c 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -452,6 +452,7 @@ public class Constants { "cluster:monitor/xpack/rollup/get/caps", "cluster:monitor/xpack/searchable_snapshots/stats", "cluster:monitor/xpack/security/saml/metadata", + "cluster:monitor/xpack/security/stats", "cluster:monitor/xpack/spatial/stats", "cluster:monitor/xpack/sql/async/status", // org.elasticsearch.xpack.core.sql.SqlAsyncActionNames.SQL_ASYNC_GET_STATUS_ACTION_NAME "cluster:monitor/xpack/sql/stats/dist", diff --git a/x-pack/plugin/security/src/main/java/module-info.java b/x-pack/plugin/security/src/main/java/module-info.java index 88b421e1efe31..1e4b54148234a 100644 --- a/x-pack/plugin/security/src/main/java/module-info.java +++ b/x-pack/plugin/security/src/main/java/module-info.java @@ -66,6 +66,7 @@ exports org.elasticsearch.xpack.security.action.token to org.elasticsearch.server; exports org.elasticsearch.xpack.security.action.user to org.elasticsearch.server; exports org.elasticsearch.xpack.security.action.settings to org.elasticsearch.server; + exports org.elasticsearch.xpack.security.action.stats to org.elasticsearch.server; exports org.elasticsearch.xpack.security.operator to org.elasticsearch.internal.operator, org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authz to org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent, org.elasticsearch.internal.security; 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 f94a699511be3..c36c9b3b350fe 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 @@ -181,6 +181,7 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction; import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction; import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; @@ -281,6 +282,7 @@ import org.elasticsearch.xpack.security.action.settings.TransportGetSecuritySettingsAction; import org.elasticsearch.xpack.security.action.settings.TransportReloadRemoteClusterCredentialsAction; import org.elasticsearch.xpack.security.action.settings.TransportUpdateSecuritySettingsAction; +import org.elasticsearch.xpack.security.action.stats.TransportSecurityStatsAction; import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction; @@ -402,6 +404,7 @@ import org.elasticsearch.xpack.security.rest.action.service.RestGetServiceAccountCredentialsAction; import org.elasticsearch.xpack.security.rest.action.settings.RestGetSecuritySettingsAction; import org.elasticsearch.xpack.security.rest.action.settings.RestUpdateSecuritySettingsAction; +import org.elasticsearch.xpack.security.rest.action.stats.RestSecurityStatsAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction; @@ -1731,6 +1734,7 @@ public List getActions() { new ActionHandler(UpdateSecuritySettingsAction.INSTANCE, TransportUpdateSecuritySettingsAction.class), new ActionHandler(ActionTypes.RELOAD_REMOTE_CLUSTER_CREDENTIALS_ACTION, TransportReloadRemoteClusterCredentialsAction.class), new ActionHandler(UpdateIndexMigrationVersionAction.INSTANCE, UpdateIndexMigrationVersionAction.TransportAction.class), + new ActionHandler(GetSecurityStatsAction.INSTANCE, TransportSecurityStatsAction.class), usageAction, infoAction ).filter(Objects::nonNull).toList(); @@ -1823,7 +1827,8 @@ public List getRestHandlers( new RestEnableProfileAction(settings, getLicenseState()), new RestDisableProfileAction(settings, getLicenseState()), new RestGetSecuritySettingsAction(settings, getLicenseState()), - new RestUpdateSecuritySettingsAction(settings, getLicenseState()) + new RestUpdateSecuritySettingsAction(settings, getLicenseState()), + new RestSecurityStatsAction(settings, getLicenseState(), clusterSupportsFeature) ).filter(Objects::nonNull).toList(); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java index 409ab62ae3e70..8c93003def79f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java @@ -16,8 +16,10 @@ public class SecurityFeatures implements FeatureSpecification { + public static final NodeFeature SECURITY_STATS_ENDPOINT = new NodeFeature("security_stats_endpoint"); + @Override public Set getFeatures() { - return Set.of(QUERYABLE_BUILT_IN_ROLES_FEATURE); + return Set.of(QUERYABLE_BUILT_IN_ROLES_FEATURE, SECURITY_STATS_ENDPOINT); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/stats/TransportSecurityStatsAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/stats/TransportSecurityStatsAction.java new file mode 100644 index 0000000000000..96cd95a800c3c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/stats/TransportSecurityStatsAction.java @@ -0,0 +1,75 @@ +/* + * 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.action.stats; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.injection.guice.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsAction; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsNodeRequest; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsNodeResponse; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsNodesRequest; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsNodesResponse; + +import java.io.IOException; +import java.util.List; + +public class TransportSecurityStatsAction extends TransportNodesAction< + GetSecurityStatsNodesRequest, + GetSecurityStatsNodesResponse, + GetSecurityStatsNodeRequest, + GetSecurityStatsNodeResponse, + Void> { + + @Inject + public TransportSecurityStatsAction( + ThreadPool threadPool, + ClusterService clusterService, + TransportService transportService, + ActionFilters actionFilters + ) { + super( + GetSecurityStatsAction.INSTANCE.name(), + clusterService, + transportService, + actionFilters, + GetSecurityStatsNodeRequest::new, + threadPool.executor(ThreadPool.Names.MANAGEMENT) + ); + } + + @Override + protected GetSecurityStatsNodesResponse newResponse( + final GetSecurityStatsNodesRequest request, + final List responses, + final List failures + ) { + return new GetSecurityStatsNodesResponse(clusterService.getClusterName(), responses, failures); + } + + @Override + protected GetSecurityStatsNodeRequest newNodeRequest(final GetSecurityStatsNodesRequest request) { + return new GetSecurityStatsNodeRequest(); + } + + @Override + protected GetSecurityStatsNodeResponse newNodeResponse(final StreamInput in, final DiscoveryNode node) throws IOException { + return new GetSecurityStatsNodeResponse(in); + } + + @Override + protected GetSecurityStatsNodeResponse nodeOperation(final GetSecurityStatsNodeRequest request, final Task task) { + return new GetSecurityStatsNodeResponse(clusterService.localNode()); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/stats/RestSecurityStatsAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/stats/RestSecurityStatsAction.java new file mode 100644 index 0000000000000..435b6a01b6faa --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/stats/RestSecurityStatsAction.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.rest.action.stats; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.Scope; +import org.elasticsearch.rest.ServerlessScope; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsAction; +import org.elasticsearch.xpack.core.security.action.stats.GetSecurityStatsNodesRequest; +import org.elasticsearch.xpack.security.SecurityFeatures; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.util.List; +import java.util.function.Predicate; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +@ServerlessScope(Scope.INTERNAL) +public class RestSecurityStatsAction extends SecurityBaseRestHandler { + + private final Predicate clusterSupportsFeature; + + public RestSecurityStatsAction( + final Settings settings, + final XPackLicenseState licenseState, + final Predicate clusterSupportsFeature + ) { + super(settings, licenseState); + this.clusterSupportsFeature = clusterSupportsFeature; + } + + @Override + public List routes() { + return List.of(new Route(GET, "/_security/stats")); + } + + @Override + public String getName() { + return "security_stats_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) { + if (clusterSupportsFeature.test(SecurityFeatures.SECURITY_STATS_ENDPOINT) == false) { + throw new IllegalArgumentException("endpoint not supported on all nodes in the cluster"); + } + final var req = new GetSecurityStatsNodesRequest(); + return channel -> client.execute(GetSecurityStatsAction.INSTANCE, req, new RestToXContentListener<>(channel)); + } + +} diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/security/stats/10_skeleton.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/security/stats/10_skeleton.yml new file mode 100644 index 0000000000000..512e7c1f6e474 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/security/stats/10_skeleton.yml @@ -0,0 +1,12 @@ +--- +"Security stats returns empty response": + - requires: + cluster_features: [ "security_stats_endpoint" ] + reason: Introduced in 9.2 + + - do: + security.get_stats: {} + + - set: + nodes._arbitrary_key_: node_id + - length: { nodes.$node_id: 0 }