Skip to content

Commit 5d1035d

Browse files
New command: 'authentication_attempts stats'
1 parent a28ab62 commit 5d1035d

File tree

7 files changed

+121
-31
lines changed

7 files changed

+121
-31
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
## v2.17.0 (in development)
44

5-
No changes yet.
5+
### Enhancements
6+
7+
* New command, `auth_attempts stats`, displays authentication attempte statistics per protocol:
8+
9+
```
10+
rabbitmqadmin auth_attempts stats --node rabbit@target.hostname
11+
```
612

713

814
## v2.16.0 (Oct 20, 2025)

Cargo.lock

Lines changed: 28 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ reqwest = { version = "0.12", features = [
1717
"__rustls",
1818
"rustls-tls-native-roots",
1919
] }
20-
rabbitmq_http_client = { version = "0.66.0", features = [
20+
rabbitmq_http_client = { version = "0.68.0", features = [
2121
"blocking",
2222
"tabled",
2323
] }

src/cli.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ pub fn parser(pre_flight_settings: PreFlightSettings) -> Command {
4646
GITHUB_REPOSITORY_URL
4747
);
4848

49+
let auth_attempts_group = Command::new("auth_attempts")
50+
.about("Operations on authentication attempt statistics")
51+
.infer_subcommands(pre_flight_settings.infer_subcommands)
52+
.infer_long_args(pre_flight_settings.infer_long_options)
53+
.after_help(color_print::cformat!(
54+
"<bold>Doc guide</bold>: {}",
55+
ACCESS_CONTROL_GUIDE_URL
56+
))
57+
.arg_required_else_help(true)
58+
.subcommands(auth_attempts_subcommands(pre_flight_settings.clone()));
4959
let bindings_group = Command::new("bindings")
5060
.about("Operations on bindings")
5161
.infer_subcommands(pre_flight_settings.infer_subcommands)
@@ -387,6 +397,7 @@ pub fn parser(pre_flight_settings: PreFlightSettings) -> Command {
387397
.subcommands(vhost_limits_subcommands(pre_flight_settings.clone()));
388398

389399
let command_groups = [
400+
auth_attempts_group,
390401
bindings_group,
391402
channels_group,
392403
close_group,
@@ -1403,6 +1414,26 @@ fn purge_subcommands(pre_flight_settings: PreFlightSettings) -> Vec<Command> {
14031414
.collect()
14041415
}
14051416

1417+
fn auth_attempts_subcommands(pre_flight_settings: PreFlightSettings) -> Vec<Command> {
1418+
let stats_cmd = Command::new("stats")
1419+
.about("Displays authentication attempt statistics for a cluster node")
1420+
.after_help(color_print::cformat!(
1421+
"<bold>Doc guide</bold>: {}",
1422+
ACCESS_CONTROL_GUIDE_URL
1423+
))
1424+
.arg(
1425+
Arg::new("node")
1426+
.long("node")
1427+
.help("target node, must be a cluster member")
1428+
.required(true),
1429+
);
1430+
1431+
[stats_cmd]
1432+
.into_iter()
1433+
.map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options))
1434+
.collect()
1435+
}
1436+
14061437
fn binding_subcommands(pre_flight_settings: PreFlightSettings) -> Vec<Command> {
14071438
let idempotently_arg = Arg::new("idempotently")
14081439
.long("idempotently")

src/commands.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ pub fn list_nodes(client: APIClient) -> ClientResult<Vec<responses::ClusterNode>
6868
client.list_nodes()
6969
}
7070

71+
pub fn list_auth_attempts(
72+
client: APIClient,
73+
command_args: &ArgMatches,
74+
) -> ClientResult<Vec<responses::AuthenticationAttemptStatistics>> {
75+
let node = command_args.get_one::<String>("node").unwrap();
76+
client.auth_attempts_statistics(node)
77+
}
78+
7179
pub fn list_vhosts(client: APIClient) -> ClientResult<Vec<responses::VirtualHost>> {
7280
client.list_vhosts()
7381
}

src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ fn dispatch_common_subcommand(
427427
res_handler: &mut ResultHandler,
428428
) -> ExitCode {
429429
match &pair {
430+
("auth_attempts", "stats") => {
431+
let result = commands::list_auth_attempts(client, second_level_args);
432+
res_handler.tabular_result(result)
433+
}
430434
("bindings", "declare") => {
431435
let result = commands::declare_binding(client, &vhost, second_level_args);
432436
res_handler.no_output_on_success(result);

tests/auth_attempts_tests.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use predicates::prelude::*;
16+
use std::error::Error;
17+
18+
mod test_helpers;
19+
use crate::test_helpers::*;
20+
21+
#[test]
22+
fn test_list_auth_attempts() -> Result<(), Box<dyn Error>> {
23+
let rc = api_client();
24+
let nodes = rc.list_nodes()?;
25+
let first = nodes.first().unwrap();
26+
27+
run_succeeds(["auth_attempts", "stats", "--node", first.name.as_str()]).stdout(
28+
output_includes("Protocol")
29+
.and(output_includes("Number of attempts"))
30+
.and(output_includes("Successful"))
31+
.and(output_includes("Failed")),
32+
);
33+
34+
Ok(())
35+
}
36+
37+
#[test]
38+
fn test_list_auth_attempts_requires_node_argument() -> Result<(), Box<dyn Error>> {
39+
run_fails(["auth_attempts", "stats"]).stderr(output_includes("--node"));
40+
41+
Ok(())
42+
}

0 commit comments

Comments
 (0)