From ebe74d2eb081a2a4592d2211d98be64d09ad8899 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Tue, 25 Jan 2022 11:25:40 +0100 Subject: [PATCH] disallow organization deletion when accounts are members, allow removal of accounts from organization (#4773) --- docs/docs/services/organizations.rst | 2 +- moto/organizations/models.py | 11 +++++ moto/organizations/responses.py | 7 +++ .../test_organizations_boto3.py | 43 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/docs/services/organizations.rst b/docs/docs/services/organizations.rst index 59b842141..18e9b284c 100644 --- a/docs/docs/services/organizations.rst +++ b/docs/docs/services/organizations.rst @@ -71,7 +71,7 @@ organizations - [X] list_targets_for_policy - [X] move_account - [X] register_delegated_administrator -- [ ] remove_account_from_organization +- [X] remove_account_from_organization - [X] tag_resource - [X] untag_resource - [X] update_organizational_unit diff --git a/moto/organizations/models.py b/moto/organizations/models.py index d2be6f530..97bcb0224 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -381,6 +381,11 @@ class OrganizationsBackend(BaseBackend): return self.org.describe() def delete_organization(self, **kwargs): + if [account for account in self.accounts if account.name != "master"]: + raise RESTError( + "OrganizationNotEmptyException", + "To delete an organization you must first remove all member accounts (except the master).", + ) self._reset() return {} @@ -885,5 +890,11 @@ class OrganizationsBackend(BaseBackend): else: raise InvalidInputException("You specified an invalid value.") + def remove_account_from_organization(self, **kwargs): + account = self.get_account_by_id(kwargs["AccountId"]) + for policy in account.attached_policies: + policy.attachments.remove(account) + self.accounts.remove(account) + organizations_backend = OrganizationsBackend() diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 686f07e34..049878db3 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -215,3 +215,10 @@ class OrganizationsResponse(BaseResponse): return json.dumps( self.organizations_backend.detach_policy(**self.request_params) ) + + def remove_account_from_organization(self): + return json.dumps( + self.organizations_backend.remove_account_from_organization( + **self.request_params + ) + ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index ea9c27c55..6914fadec 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -420,6 +420,49 @@ def test_get_paginated_list_create_account_status(): validate_create_account_status(createAccountStatus) +@mock_organizations +def test_remove_account_from_organization(): + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL")["Organization"] + create_account_status = client.create_account( + AccountName=mockname, Email=mockemail + )["CreateAccountStatus"] + account_id = create_account_status["AccountId"] + + def created_account_exists(accounts): + return any( + account + for account in accounts + if account["Name"] == mockname and account["Email"] == mockemail + ) + + accounts = client.list_accounts()["Accounts"] + assert len(accounts) == 2 + assert created_account_exists(accounts) + client.remove_account_from_organization(AccountId=account_id) + accounts = client.list_accounts()["Accounts"] + assert len(accounts) == 1 + assert not created_account_exists(accounts) + + +@mock_organizations +def test_delete_organization_with_existing_account(): + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + create_account_status = client.create_account( + Email=mockemail, AccountName=mockname + )["CreateAccountStatus"] + account_id = create_account_status["AccountId"] + with pytest.raises(ClientError) as e: + client.delete_organization() + e.match("OrganizationNotEmptyException") + client.remove_account_from_organization(AccountId=account_id) + client.delete_organization() + with pytest.raises(ClientError) as e: + client.describe_organization() + e.match("AWSOrganizationsNotInUseException") + + # Service Control Policies policy_doc01 = dict( Version="2012-10-17",