Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
24b3ac1
feat(organizations): support many independent organizations per process
vieiralucas Sep 24, 2026
425c0a7
fix(organizations): correct the responsibility-transfer and handshake…
vieiralucas Sep 24, 2026
945eb2c
fix(organizations): scope handshake duplicates and let either party e…
vieiralucas Sep 24, 2026
05dea1a
fix(organizations): resolve email targets against registered addresses
vieiralucas Sep 24, 2026
9faf9e7
fix(organizations): make transfer targets answerable and account ids …
vieiralucas Sep 24, 2026
af6b33e
fix(organizations): scope email resolution and render handshake parti…
vieiralucas Sep 24, 2026
d04b7a0
fix(organizations): a responsibility transfer targets another organiz…
vieiralucas Sep 24, 2026
1563a1e
fix(organizations): record transfer targets as named, resolve the cal…
vieiralucas Sep 24, 2026
1546b7d
refactor(organizations): one predicate decides who a handshake target is
vieiralucas Sep 24, 2026
807c323
fix(organizations): compare organizations, not management accounts, o…
vieiralucas Sep 24, 2026
981cdff
fix(organizations): close the email spelling of a self-targeted transfer
vieiralucas Sep 24, 2026
a028766
fix(organizations): emit a resolvable transfer target id, drop the in…
vieiralucas Sep 24, 2026
a93c0bd
fix(organizations): a reserved account id is claimed, and accept enro…
vieiralucas Sep 24, 2026
4c0a07f
fix(organizations): honour the reservation window on every enrolling …
vieiralucas Sep 24, 2026
7af88f3
fix(organizations): prefer the registered address, and scope effectiv…
vieiralucas Sep 24, 2026
27790fc
fix(organizations): an address names exactly one account
vieiralucas Sep 24, 2026
6c46a64
docs(organizations): describe the resolution rules the code actually …
vieiralucas Sep 24, 2026
2121178
fix(organizations): give the GovCloud mirror its own address, agree o…
vieiralucas Sep 24, 2026
dfbefc1
fix(organizations): resolve the transfer target before judging it, an…
vieiralucas Sep 24, 2026
38ad9a8
fix(organizations): enforce unique account addresses, restore the mod…
vieiralucas Sep 24, 2026
60154a9
fix(organizations): authorize before validating, and settle the trans…
vieiralucas Sep 24, 2026
0453037
fix(organizations): fail a duplicate address the way AWS does, and re…
vieiralucas Sep 24, 2026
b13d7e3
fix(organizations): first caller wins a contested address, closed acc…
vieiralucas Sep 24, 2026
579c93c
fix(organizations): reserve a synthetic address for the id it spells
vieiralucas Sep 24, 2026
9104ea4
fix(organizations): apply the address rules on the CloudFormation pat…
vieiralucas Sep 24, 2026
7bd5dfc
fix(organizations): read an ACCOUNT transfer target as an id, authori…
vieiralucas Sep 24, 2026
6b54e35
fix(organizations): check the handshake's state before its membership…
vieiralucas Sep 24, 2026
507163b
fix(organizations): party gate before state, and list the handshakes …
vieiralucas Sep 24, 2026
7983889
fix(organizations): accept TRANSFER_RESPONSIBILITY as a handshake fil…
vieiralucas Sep 24, 2026
3c60589
fix(organizations): open org reads to delegated administrators, and c…
vieiralucas Sep 24, 2026
af5e157
fix(organizations): end a delegated-administrator grant with the memb…
vieiralucas Sep 24, 2026
7733614
fix(organizations): name the account in AccountNotRegisteredException
vieiralucas Sep 24, 2026
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
4 changes: 3 additions & 1 deletion crates/fakecloud-cloudformation/src/extras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2941,7 +2941,9 @@ pub(crate) mod tests {
ecr: shared::<EcrState>(),
cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
organizations: Arc::new(RwLock::new(None)),
organizations: Arc::new(RwLock::new(
fakecloud_organizations::OrganizationsRegistry::default(),
)),
cognito: shared::<fakecloud_cognito::CognitoState>(),
rds: shared::<fakecloud_rds::RdsState>(),
ec2: shared::<fakecloud_ec2::Ec2State>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4117,7 +4117,7 @@ mod tests {
)),
cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
organizations_state: Arc::new(RwLock::new(None)),
organizations_state: Arc::new(RwLock::new(fakecloud_organizations::OrganizationsRegistry::default())),
cognito_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
Expand Down Expand Up @@ -4676,7 +4676,7 @@ mod tests {
.expect("org provisions");
let root_id = {
let g = prov.organizations_state.read();
g.as_ref().unwrap().root_id.clone()
g.sole().unwrap().root_id.clone()
};

let ou = prov
Expand Down Expand Up @@ -4756,7 +4756,7 @@ mod tests {
);

let g = prov.organizations_state.read();
let org = g.as_ref().unwrap();
let org = g.sole().unwrap();
assert_eq!(org.ous.get(&ou_id).unwrap().name, "team-renamed");
assert_eq!(org.policies.get(&pol_id).unwrap().content, "{\"v\":2}");
assert!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,31 @@ impl ResourceProvisioner {
.to_string();

let mut org = self.organizations_state.write();
if org.is_some() {
return Err("Organization already exists; only one per fakecloud process".to_string());
// Only the stack's own account blocks this. Organizations are
// independent, so another account having one must not stop this
// stack from creating its own (#2543).
if org.account_is_enrolled(&self.account_id) {
return Err(format!(
"Account {} is already a member of an organization",
self.account_id
));
}
// The management account registers its own synthetic address, so
// that address must be free -- the same rule the API's
// `CreateOrganization` applies.
let management_email = format!("{}@example.com", self.account_id);
if org.email_in_use(&management_email) {
return Err(format!(
"The email address {management_email} is already associated with another account"
));
}
let mut state = OrganizationState::bootstrap(&self.account_id);
state.feature_set = feature_set;
let org_id = state.org_id.clone();
let org_arn = state.org_arn.clone();
let mgmt_arn = state.management_account_arn.clone();
let root_id = state.root_id.clone();
*org = Some(state);
org.insert(state);

Ok(ProvisionResult::new(org_id.clone())
.with("Id", org_id)
Expand All @@ -35,9 +50,11 @@ impl ResourceProvisioner {
.with("RootId", root_id))
}

pub(crate) fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
pub(crate) fn delete_organization(&self, physical_id: &str) -> Result<(), String> {
// The physical id IS the organization id, so delete exactly the
// one this stack created rather than every organization.
let mut org = self.organizations_state.write();
*org = None;
org.remove(physical_id);
Ok(())
}

Expand All @@ -60,7 +77,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
// Accept root id, OU id, or `Ref`-resolved logical id (we map to root).
let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
Expand Down Expand Up @@ -96,7 +113,7 @@ impl ResourceProvisioner {

pub(crate) fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
if let Some(org) = org_lock.org_of_account_mut(&self.account_id) {
org.ous.remove(physical_id);
org.attachments.remove(physical_id);
}
Expand Down Expand Up @@ -148,14 +165,43 @@ impl ResourceProvisioner {
.unwrap_or_default();

let mut org_lock = self.organizations_state.write();
// Authorize FIRST, as the API paths do: the address checks below
// span the registry, so running them before resolving the stack
// account's own organization would report whether an address is
// registered in an organization this stack has nothing to do with.
if org_lock.org_of_account(&self.account_id).is_none() {
return Err("Organization not yet created".to_string());
}
// Same address-uniqueness rule the API enforces: resolution by
// address decides who may accept an EMAIL-targeted handshake, so
// two accounts sharing one would make that answer depend on id
// ordering.
if org_lock.email_in_use(&email) {
return Err(format!(
"The email address {email} is already associated with an account"
));
}
// Mint from the registry so the id cannot collide with an account
// another organization already owns.
let new_account_id = org_lock.next_account_id();
// ...and a `<account-id>@example.com` address belongs to the id it
// spells, so this account cannot squat another one's.
if fakecloud_organizations::OrganizationsRegistry::email_reserved_for_other(
&email,
&new_account_id,
) {
return Err(format!(
"The email address {email} is reserved for another account"
));
}
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
// CFN provisioning is its own asynchronous flow; we don't need
// a second layer of poll-for-completion on top. Begin the
// request and immediately drive it to SUCCEEDED so the rest of
// this provisioner sees a fully enrolled account.
let pending = org.begin_create_account(&email, &name, None);
let pending = org.begin_create_account(&email, &name, new_account_id, None);
let status = org.complete_create_account(&pending.id).unwrap_or(pending);
let account_id = status
.account_id
Expand Down Expand Up @@ -216,7 +262,7 @@ impl ResourceProvisioner {
/// `close_account` so subsequent reads see it as suspended.
pub(crate) fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
if let Some(org) = org_lock.org_of_account_mut(&self.account_id) {
let _ = org.close_account(physical_id);
}
Ok(())
Expand Down Expand Up @@ -265,7 +311,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
let id_suffix: String = Uuid::new_v4()
.simple()
Expand Down Expand Up @@ -307,7 +353,7 @@ impl ResourceProvisioner {

pub(crate) fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
if let Some(org) = org_lock.org_of_account_mut(&self.account_id) {
org.policies.remove(physical_id);
for attachments in org.attachments.values_mut() {
attachments.remove(physical_id);
Expand All @@ -334,7 +380,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
org.resource_policy = Some(content);
let arn = format!(
Expand All @@ -349,7 +395,7 @@ impl ResourceProvisioner {
_physical_id: &str,
) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
if let Some(org) = org_lock.org_of_account_mut(&self.account_id) {
org.resource_policy = None;
}
Ok(())
Expand Down Expand Up @@ -378,7 +424,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
let ou = org
.ous
Expand Down Expand Up @@ -430,7 +476,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
// AccountName/Email are immutable; do NOT mint a new account. Move to a
// new parent if ParentIds changed and refresh tags in place.
Expand Down Expand Up @@ -504,7 +550,7 @@ impl ResourceProvisioner {

let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.org_of_account_mut(&self.account_id)
.ok_or_else(|| "Organization not yet created".to_string())?;
let (arn, name) = {
let policy = org
Expand Down
4 changes: 3 additions & 1 deletion crates/fakecloud-cloudformation/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4233,7 +4233,9 @@ mod tests {
)),
cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
organizations: Arc::new(RwLock::new(None)),
organizations: Arc::new(RwLock::new(
fakecloud_organizations::OrganizationsRegistry::default(),
)),
cognito: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
Expand Down
Loading
Loading