1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//! AWS Organizations: enumerating member accounts for the
//! multi-account overlays.
//!
//! The service is global, but the client is built from the operator's
//! own `SdkConfig` — unlike IAM and Cost Explorer, nothing here pins a
//! region; the SDK's endpoint rules route it.
use super::*;
/// One row in the `:accounts` overlay — an AWS Organizations child
/// account (or the management account itself). Sourced from
/// `organizations:ListAccounts`.
#[derive(Clone, Debug)]
pub struct OrgAccount {
/// 12-digit account ID.
pub id: String,
/// Friendly name set when the account joined the org.
pub name: String,
/// Root user's email address (often the only way to spot ownership
/// when account names are terse).
pub email: Option<String>,
/// `ACTIVE` / `SUSPENDED` / `PENDING_CLOSURE` — capitalised verbatim
/// from the API.
pub status: String,
}
impl AwsClient {
/// `organizations:ListAccounts`, paginated. Returns every active +
/// suspended account the active credentials can see (i.e. the
/// caller is in the mgmt account or a delegated administrator).
/// Surfaces the API's `AccessDenied` cleanly so the `:accounts`
/// overlay can render a "no org access" hint rather than an opaque
/// stack trace.
pub async fn list_org_accounts(&self) -> Result<Vec<OrgAccount>> {
let this = self;
// `SCAN_PAGES`, not the default runaway guard: ListAccounts
// caps `MaxResults` at 20, so `MAX_PAGES` put a hard 2,000-
// account ceiling on this — and since the walk `.complete()`s,
// an org past that ceiling got an error instead of a list.
// 500 pages is 10,000 accounts, past the highest quota AWS
// will raise an organization to.
let raw = super::paginate_capped(
"organizations:ListAccounts",
super::SCAN_PAGES,
move |token| async move {
// 20 is the API maximum for this call, not a choice.
let mut req = this.org().list_accounts().max_results(20);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req
.send()
.await
.wrap_err("organizations:ListAccounts failed")?;
Ok((resp.accounts.unwrap_or_default(), resp.next_token))
},
)
.await?
// `:accounts` / `:find-env` search this list by name; a short
// one means "no such account" for an account that exists.
.complete("organizations:ListAccounts")?;
let mut out: Vec<OrgAccount> = raw
.into_iter()
.map(|a| OrgAccount {
id: a.id.unwrap_or_default(),
name: a.name.unwrap_or_default(),
email: a.email,
status: a.status.map(|s| s.as_str().to_string()).unwrap_or_default(),
})
.collect();
// Stable display order: status (Active first), then name.
out.sort_by(|a, b| {
let sa = (a.status != "ACTIVE", a.name.to_lowercase());
let sb = (b.status != "ACTIVE", b.name.to_lowercase());
sa.cmp(&sb)
});
Ok(out)
}
}