import json from unittest import SkipTest, TestCase from uuid import uuid4 import boto3 import pytest from botocore.exceptions import ClientError from moto import mock_aws, settings from moto.cloudformation import cloudformation_backends as cf_backends from moto.core import DEFAULT_ACCOUNT_ID from moto.sqs import sqs_backends TEMPLATE_DCT = { "AWSTemplateFormatVersion": "2010-09-09", "Description": "Template creating one queue", "Resources": { "MyQueue": {"Type": "AWS::SQS::Queue", "Properties": {"QueueName": "testqueue"}} }, } class TestStackSetMultipleAccounts(TestCase): def setUp(self) -> None: # Set up multiple organizations/accounts # Our tests will create resources within these orgs/accounts # # | ROOT | # / \ # / \ # [ou01] [ou02] # | / | \ # | / [acct02] \ # [acct01] / \ # [ou21] [ou22] # | | # [acct21] [acct22] # if settings.TEST_SERVER_MODE: raise SkipTest( "These tests directly access the backend, which is not possible in ServerMode" ) mockname = "mock-account" mockdomain = "moto-example.org" mockemail = "@".join([mockname, mockdomain]) self.org = boto3.client("organizations", region_name="us-east-1") self.org.create_organization(FeatureSet="ALL") self.acct01 = self.org.create_account(AccountName=mockname, Email=mockemail)[ "CreateAccountStatus" ]["AccountId"] self.acct02 = self.org.create_account(AccountName=mockname, Email=mockemail)[ "CreateAccountStatus" ]["AccountId"] self.acct21 = self.org.create_account(AccountName=mockname, Email=mockemail)[ "CreateAccountStatus" ]["AccountId"] self.acct22 = self.org.create_account(AccountName=mockname, Email=mockemail)[ "CreateAccountStatus" ]["AccountId"] root_id = self.org.list_roots()["Roots"][0]["Id"] self.ou01 = self.org.create_organizational_unit(ParentId=root_id, Name="ou1")[ "OrganizationalUnit" ]["Id"] self.ou02 = self.org.create_organizational_unit(ParentId=root_id, Name="ou2")[ "OrganizationalUnit" ]["Id"] self.ou21 = self.org.create_organizational_unit( ParentId=self.ou02, Name="ou021" )["OrganizationalUnit"]["Id"] self.ou22 = self.org.create_organizational_unit( ParentId=self.ou02, Name="ou022" )["OrganizationalUnit"]["Id"] self.org.move_account( AccountId=self.acct01, SourceParentId=root_id, DestinationParentId=self.ou01 ) self.org.move_account( AccountId=self.acct02, SourceParentId=root_id, DestinationParentId=self.ou02 ) self.org.move_account( AccountId=self.acct21, SourceParentId=root_id, DestinationParentId=self.ou21 ) self.org.move_account( AccountId=self.acct22, SourceParentId=root_id, DestinationParentId=self.ou22 ) self.name = "a" + str(uuid4())[0:6] self.client = boto3.client("cloudformation", region_name="us-east-1") def _verify_stack_instance(self, accnt, region=None): resp = self.client.describe_stack_instance( StackSetName=self.name, StackInstanceAccount=accnt, StackInstanceRegion=region or "us-east-1", )["StackInstance"] assert resp["Account"] == accnt def _verify_queues(self, accnt, expected, region=None): assert ( list(sqs_backends[accnt][region or "us-east-1"].queues.keys()) == expected ) @mock_aws class TestServiceManagedStacks(TestStackSetMultipleAccounts): def setUp(self) -> None: super().setUp() self.client.create_stack_set( StackSetName=self.name, Description="test creating stack set in multiple accounts", TemplateBody=json.dumps(TEMPLATE_DCT), PermissionModel="SERVICE_MANAGED", AutoDeployment={"Enabled": False}, ) def test_create_instances__specifying_accounts(self): with pytest.raises(ClientError) as exc: self.client.create_stack_instances( StackSetName=self.name, Accounts=["888781156701"], Regions=["us-east-1"] ) err = exc.value.response["Error"] assert err["Code"] == "ValidationError" assert ( err["Message"] == "StackSets with SERVICE_MANAGED permission model can only have OrganizationalUnit as target" ) def test_create_instances__specifying_only_accounts_in_deployment_targets(self): with pytest.raises(ClientError) as exc: self.client.create_stack_instances( StackSetName=self.name, DeploymentTargets={ "Accounts": ["888781156701"], }, Regions=["us-east-1"], ) err = exc.value.response["Error"] assert err["Code"] == "ValidationError" assert err["Message"] == "OrganizationalUnitIds are required" def test_create_instances___with_invalid_ou(self): with pytest.raises(ClientError) as exc: self.client.create_stack_instances( StackSetName=self.name, DeploymentTargets={"OrganizationalUnitIds": ["unknown"]}, Regions=["us-east-1"], ) err = exc.value.response["Error"] assert err["Code"] == "ValidationError" assert ( err["Message"] == "1 validation error detected: Value '[unknown]' at 'deploymentTargets.organizationalUnitIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 68, Member must have length greater than or equal to 6, Member must satisfy regular expression pattern: ^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})$]" ) def test_create_instances__single_ou(self): self.client.create_stack_instances( StackSetName=self.name, DeploymentTargets={"OrganizationalUnitIds": [self.ou01]}, Regions=["us-east-1"], ) self._verify_stack_instance(self.acct01) # We have a queue in our account connected to OU-01 self._verify_queues(self.acct01, ["testqueue"]) # No queue has been created in our main account, or unrelated accounts self._verify_queues(DEFAULT_ACCOUNT_ID, []) self._verify_queues(self.acct02, []) self._verify_queues(self.acct21, []) self._verify_queues(self.acct22, []) def test_create_instances__ou_with_kiddos(self): self.client.create_stack_instances( StackSetName=self.name, DeploymentTargets={"OrganizationalUnitIds": [self.ou02]}, Regions=["us-east-1"], ) self._verify_stack_instance(self.acct02) self._verify_stack_instance(self.acct21) self._verify_stack_instance(self.acct22) # No queue has been created in our main account self._verify_queues(DEFAULT_ACCOUNT_ID, expected=[]) self._verify_queues(self.acct01, expected=[]) # But we do have a queue in our (child)-accounts of OU-02 self._verify_queues(self.acct02, ["testqueue"]) self._verify_queues(self.acct21, ["testqueue"]) self._verify_queues(self.acct22, ["testqueue"]) @mock_aws class TestSelfManagedStacks(TestStackSetMultipleAccounts): def setUp(self) -> None: super().setUp() # Assumptions: All the necessary IAM roles are created # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html self.client.create_stack_set( StackSetName=self.name, Description="test creating stack set in multiple accounts", TemplateBody=json.dumps(TEMPLATE_DCT), ) def test_create_instances__specifying_accounts(self): self.client.create_stack_instances( StackSetName=self.name, Accounts=[self.acct01], Regions=["us-east-2"] ) self._verify_stack_instance(self.acct01, region="us-east-2") # acct01 has a Stack assert len(cf_backends[self.acct01]["us-east-2"].stacks) == 1 # Other acounts do not assert len(cf_backends[self.acct02]["us-east-2"].stacks) == 0 assert len(cf_backends[self.acct21]["us-east-2"].stacks) == 0 assert len(cf_backends[self.acct22]["us-east-2"].stacks) == 0 assert len(cf_backends[DEFAULT_ACCOUNT_ID]["us-east-2"].stacks) == 0 # acct01 has a queue self._verify_queues(self.acct01, ["testqueue"], region="us-east-2") # other accounts are empty self._verify_queues(self.acct02, [], region="us-east-2") self._verify_queues(self.acct21, [], region="us-east-2") self._verify_queues(self.acct22, [], region="us-east-2") self._verify_queues(DEFAULT_ACCOUNT_ID, [], region="us-east-2") def test_create_instances__multiple_accounts(self): self.client.create_stack_instances( StackSetName=self.name, Accounts=[self.acct01, self.acct02], Regions=["us-east-2"], ) # acct01 and 02 have a Stack assert len(cf_backends[self.acct01]["us-east-2"].stacks) == 1 assert len(cf_backends[self.acct02]["us-east-2"].stacks) == 1 # Other acounts do not assert len(cf_backends[self.acct21]["us-east-2"].stacks) == 0 assert len(cf_backends[self.acct22]["us-east-2"].stacks) == 0 assert len(cf_backends[DEFAULT_ACCOUNT_ID]["us-east-2"].stacks) == 0 # acct01 and 02 have the queue self._verify_queues(self.acct01, ["testqueue"], region="us-east-2") self._verify_queues(self.acct02, ["testqueue"], region="us-east-2") # other accounts are empty self._verify_queues(self.acct21, [], region="us-east-2") self._verify_queues(self.acct22, [], region="us-east-2") self._verify_queues(DEFAULT_ACCOUNT_ID, [], region="us-east-2") def test_delete_instances(self): # Create two stack instances self.client.create_stack_instances( StackSetName=self.name, Accounts=[self.acct01, self.acct02], Regions=["us-east-2"], ) # Delete one self.client.delete_stack_instances( StackSetName=self.name, Accounts=[self.acct01], Regions=["us-east-2"], RetainStacks=False, ) # Act02 still has it's stack assert len(cf_backends[self.acct02]["us-east-2"].stacks) == 1 # Other Stacks are removed assert len(cf_backends[self.acct01]["us-east-2"].stacks) == 0 assert len(cf_backends[self.acct21]["us-east-2"].stacks) == 0 assert len(cf_backends[self.acct22]["us-east-2"].stacks) == 0 assert len(cf_backends[DEFAULT_ACCOUNT_ID]["us-east-2"].stacks) == 0 # Acct02 still has it's queue as well self._verify_queues(self.acct02, ["testqueue"], region="us-east-2") # Other queues are removed self._verify_queues(self.acct01, [], region="us-east-2") self._verify_queues(self.acct21, [], region="us-east-2") self._verify_queues(self.acct22, [], region="us-east-2") self._verify_queues(DEFAULT_ACCOUNT_ID, [], region="us-east-2") def test_create_instances__single_ou(self): with pytest.raises(ClientError) as exc: self.client.create_stack_instances( StackSetName=self.name, DeploymentTargets={"OrganizationalUnitIds": [self.ou01]}, Regions=["us-east-1"], ) err = exc.value.response["Error"] assert err["Code"] == "ValidationError" assert ( err["Message"] == "StackSets with SELF_MANAGED permission model can only have accounts as target" )