Skip to content

Commit 460427f

Browse files
committed
docs(admin): state how the account and two-factor failures are classified
Every other workflow section in this reference says how its failures map onto exit codes; this one only mentioned the usage error for a prompt it could not offer. The table fills that in, and two notes cover the parts a script author would otherwise have to discover. The lockout is worth spelling out. It arrives as the network code, which normally means "retry", and it is retryable — but only after the delay the server reports, because the code comes from `SlowDown`. A script that retries immediately is simply refused again. A failed `--output-file` write is also called out as the one case where a non-zero exit does not mean nothing happened: the server has already issued the set, so the codes are printed and the exit code reports only that the file was not written. Two rows are pinned by tests rather than left to drift: that a lockout is classified as retryable, and that an absent route is a not-found rather than the unsupported code an up-to-date server sends when at-rest protection is unconfigured. The enrollment test that covered the latter was named for a route that does not exist, which is a different failure; it now says what it checks.
1 parent 4422abc commit 460427f

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

crates/cli/tests/admin_account.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ fn mfa_status_succeeds_against_a_server_that_answers() {
195195
}
196196

197197
#[test]
198-
fn mfa_enroll_reports_unsupported_when_the_server_has_no_such_route() {
198+
fn mfa_enroll_reports_unsupported_when_at_rest_protection_is_missing() {
199+
// 501, which the server sends when `RUSTFS_IAM_MASTER_KEY` is unset. Not to
200+
// be confused with an absent route, which is a 404 and a different code.
199201
let config_dir = tempfile::tempdir().expect("create config dir");
200202
let (endpoint, receiver, handle) = start_admin_response_test_server(
201203
"501 Not Implemented",
@@ -540,3 +542,65 @@ fn user_mfa_status_reports_auth_error_when_the_server_refuses() {
540542
.expect("captured admin request");
541543
handle.join().expect("admin test server finished");
542544
}
545+
546+
// ---------------------------------------------------------------------------
547+
// The exit-code table in docs/reference/rc/admin.md
548+
// ---------------------------------------------------------------------------
549+
550+
/// The reference documents how each failure is classified, and that file is a
551+
/// protected contract. These pin the two rows a reader is most likely to build
552+
/// retry logic around, so the table cannot drift away from the code silently.
553+
#[test]
554+
fn a_lockout_is_reported_as_retryable_rather_than_a_refusal() {
555+
// The server answers `SlowDown` once the attempt limit is reached. That maps
556+
// to the network code, which normally means "retry" — the reference says why
557+
// it means "retry later" here, and this holds it to that class.
558+
let config_dir = tempfile::tempdir().expect("create config dir");
559+
let (endpoint, receiver, handle) = start_admin_response_test_server(
560+
"503 Service Unavailable",
561+
"application/json",
562+
r#"{"Code":"SlowDown","Message":"too many attempts; try again in 900 seconds"}"#
563+
.to_string(),
564+
);
565+
566+
let output = rc()
567+
.args([
568+
"--json", "admin", "account", "mfa", "activate", "myalias", "--code", "123456",
569+
])
570+
.env("RC_CONFIG_DIR", config_dir.path())
571+
.env("RC_HOST_myalias", rc_host_alias(&endpoint))
572+
.output()
573+
.expect("run rc command");
574+
575+
assert_eq!(output.status.code(), Some(3), "expected NetworkError");
576+
receiver
577+
.recv_timeout(Duration::from_secs(5))
578+
.expect("captured admin request");
579+
handle.join().expect("admin test server finished");
580+
}
581+
582+
#[test]
583+
fn an_absent_route_is_reported_as_not_found() {
584+
// A server predating these endpoints. Distinct from the 501 an up-to-date
585+
// server sends when at-rest protection is unconfigured, which is the row
586+
// above it in the table.
587+
let config_dir = tempfile::tempdir().expect("create config dir");
588+
let (endpoint, receiver, handle) = start_admin_response_test_server(
589+
"404 Not Found",
590+
"application/json",
591+
r#"{"Code":"NoSuchKey","Message":"unknown route"}"#.to_string(),
592+
);
593+
594+
let output = rc()
595+
.args(["--json", "admin", "account", "mfa", "status", "myalias"])
596+
.env("RC_CONFIG_DIR", config_dir.path())
597+
.env("RC_HOST_myalias", rc_host_alias(&endpoint))
598+
.output()
599+
.expect("run rc command");
600+
601+
assert_eq!(output.status.code(), Some(5), "expected NotFound");
602+
receiver
603+
.recv_timeout(Duration::from_secs(5))
604+
.expect("captured admin request");
605+
handle.join().expect("admin test server finished");
606+
}

docs/reference/rc/admin.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,31 @@ asks for confirmation; `--yes` is required in `--json` mode or when stdin is not
191191
a terminal. The account is left protected by its password alone until the user
192192
enrols again.
193193

194+
### Exit codes
195+
196+
Every other workflow in this reference states how its failures are classified, so
197+
these do too. The account and two-factor commands map the server's answer onto
198+
the standard codes:
199+
200+
| Condition | Code |
201+
|---|---|
202+
| Wrong verification code, recovery code, or password | authentication (4) |
203+
| Locked out after repeated wrong codes | network (3) |
204+
| At-rest protection not configured on the server | unsupported (7) |
205+
| Route absent, on a server predating these endpoints | not found (5) |
206+
| Recovery-code output path already occupied | conflict (6) |
207+
| Missing, conflicting, or unprompted flags | usage (2) |
208+
| Already enabled, not enabled, or no pending enrollment | general (1) |
209+
210+
The lockout deserves a note, because network (3) usually means "retry". It is
211+
retryable here too, but only after the delay the server reports — the code comes
212+
from `SlowDown`, which is the S3 vocabulary's closest analogue to a rate limit.
213+
A script that retries immediately will simply be refused again.
214+
215+
A failed `--output-file` write is the one case where a non-zero exit does not mean
216+
nothing happened: the server has already issued the set, so the codes are printed
217+
and the exit code reports only that the file was not written.
218+
194219
### Root identities
195220

196221
A root identity provisioned from `RUSTFS_ACCESS_KEY` cannot have its password or

0 commit comments

Comments
 (0)