Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/135271.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 135271
summary: Add DLS stats to `_security/stats`
area: Authorization
type: enhancement
issues: []
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9176000
2 changes: 1 addition & 1 deletion server/src/main/resources/transport/upper_bounds/9.2.csv
Original file line number Diff line number Diff line change
@@ -1 +1 @@
contextual_ai_service,9175000
roles_security_stats,9176000
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,74 @@

package org.elasticsearch.xpack.core.security.action.stats;

import org.elasticsearch.TransportVersion;
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.core.Nullable;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

public class GetSecurityStatsNodeResponse extends BaseNodeResponse implements ToXContentObject {

private static final TransportVersion ROLES_SECURITY_STATS = TransportVersion.fromName("roles_security_stats");

@Nullable
private final Map<String, Object> rolesStoreStats;

public GetSecurityStatsNodeResponse(final StreamInput in) throws IOException {
super(in);
this.rolesStoreStats = in.getTransportVersion().supports(ROLES_SECURITY_STATS) ? in.readGenericMap() : null;
}

public GetSecurityStatsNodeResponse(final DiscoveryNode node) {
public GetSecurityStatsNodeResponse(final DiscoveryNode node, @Nullable final Map<String, Object> rolesStoreStats) {
super(node);
this.rolesStoreStats = rolesStoreStats;
}

@Override
public void writeTo(final StreamOutput out) throws IOException {
super.writeTo(out);
if (out.getTransportVersion().supports(ROLES_SECURITY_STATS)) {
out.writeGenericMap(rolesStoreStats);
}
}

@Override
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
if (rolesStoreStats != null) {
builder.field("roles", rolesStoreStats);
}
return builder;
}

@Override
public boolean equals(Object o) {
return this == o
|| (o instanceof GetSecurityStatsNodeResponse that
&& Objects.equals(getNode(), that.getNode())
&& Objects.equals(rolesStoreStats, that.rolesStoreStats));
}

@Override
public int hashCode() {
return Objects.hash(getNode(), rolesStoreStats);
}

// for testing
@Nullable
Map<String, Object> getRolesStoreStats() {
return rolesStoreStats == null ? null : Collections.unmodifiableMap(rolesStoreStats);
}

// for testing
DiscoveryNode getDiscoveryNode() {
return getNode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.cluster.node.DiscoveryNodeUtils;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.test.AbstractWireSerializingTestCase;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;

public class GetSecurityStatsNodeResponseTests extends AbstractWireSerializingTestCase<GetSecurityStatsNodeResponse> {

@Override
protected Writeable.Reader<GetSecurityStatsNodeResponse> instanceReader() {
return GetSecurityStatsNodeResponse::new;
}

@Override
protected GetSecurityStatsNodeResponse createTestInstance() {
return new GetSecurityStatsNodeResponse(
DiscoveryNodeUtils.builder(randomAlphaOfLength(10)).ephemeralId(randomAlphanumericOfLength(10)).build(),
randomBoolean() ? null : Map.of("key", randomAlphaOfLength(5))
);
}

@Override
protected GetSecurityStatsNodeResponse mutateInstance(GetSecurityStatsNodeResponse instance) throws IOException {
final var node = instance.getDiscoveryNode();
final var value = Objects.requireNonNullElse(instance.getRolesStoreStats(), Map.of()).get("key");
return switch (randomIntBetween(0, 1)) {
case 0 -> new GetSecurityStatsNodeResponse(
DiscoveryNodeUtils.builder(randomValueOtherThan(node.getId(), () -> randomAlphaOfLength(10)))
// DiscoverNode#hashCode only tests ephemeralId, so make sure to change it too
.ephemeralId(randomValueOtherThan(node.getEphemeralId(), () -> randomAlphanumericOfLength(10)))
.build(),
instance.getRolesStoreStats()
);
case 1 -> new GetSecurityStatsNodeResponse(node, Map.of("key", randomValueOtherThan(value, () -> randomAlphaOfLength(5))));
default -> throw new IllegalStateException("Unexpected value");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.injection.guice.Inject;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
Expand All @@ -21,6 +22,7 @@
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 org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;

import java.io.IOException;
import java.util.List;
Expand All @@ -32,12 +34,16 @@ public class TransportSecurityStatsAction extends TransportNodesAction<
GetSecurityStatsNodeResponse,
Void> {

@Nullable
private final CompositeRolesStore rolesStore;

@Inject
public TransportSecurityStatsAction(
ThreadPool threadPool,
ClusterService clusterService,
TransportService transportService,
ActionFilters actionFilters
ActionFilters actionFilters,
CompositeRolesStore rolesStore
) {
super(
GetSecurityStatsAction.INSTANCE.name(),
Expand All @@ -47,6 +53,7 @@ public TransportSecurityStatsAction(
GetSecurityStatsNodeRequest::new,
threadPool.executor(ThreadPool.Names.MANAGEMENT)
);
this.rolesStore = rolesStore;
}

@Override
Expand All @@ -70,6 +77,6 @@ protected GetSecurityStatsNodeResponse newNodeResponse(final StreamInput in, fin

@Override
protected GetSecurityStatsNodeResponse nodeOperation(final GetSecurityStatsNodeRequest request, final Task task) {
return new GetSecurityStatsNodeResponse(clusterService.localNode());
return new GetSecurityStatsNodeResponse(clusterService.localNode(), rolesStore == null ? null : rolesStore.usageStatsWithJustDls());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,10 @@ Iterable<ProjectScoped<RoleKey>> cachedRoles() {
return this.roleCache.keys();
}

public Map<String, Object> usageStatsWithJustDls() {
return Map.of("dls", Map.of("bit_set_cache", dlsBitsetCache.usageStats()));
}

public void usageStats(ActionListener<Map<String, Object>> listener) {
final Map<String, Object> usage = new HashMap<>();
usage.put("dls", Map.of("bit_set_cache", dlsBitsetCache.usageStats()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"Security stats returns empty response":
"Security stats returns a number of stats":
- requires:
cluster_features: [ "security_stats_endpoint" ]
reason: Introduced in 9.2
Expand All @@ -9,4 +9,4 @@

- set:
nodes._arbitrary_key_: node_id
- length: { nodes.$node_id: 0 }
- length: { nodes.$node_id: 1 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"Security stats return just DLS stats":
- 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: 1 }
- length: { nodes.$node_id.roles: 1 }
- length: { nodes.$node_id.roles.dls: 1 }
- length: { nodes.$node_id.roles.dls.bit_set_cache: 8 }
- gte: { nodes.$node_id.roles.dls.bit_set_cache.count: 0 }