diff --git a/docs/admin-guide.md b/docs/admin-guide.md index 388616810..96fc61ace 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -320,12 +320,31 @@ pipelines: ``` ## Service Control Policies -Service control policies *(SCPs)* are one type of policy that you can use to manage your organization. SCPs offer central control over the maximum available permissions for all accounts in your organization, allowing you to ensure your accounts stay within your organization’s access control guidelines. ADF allows SCPs to be applied in a similar fashion as base stacks. You can define your SCP definition in a file named `scp.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account. +Service control policies *(SCPs)* are one type of policy that you can use to manage your organization. SCPs offer central control over the maximum available permissions for all accounts in your organization, allowing you to ensure your accounts stay within your organization’s access control guidelines. ADF allows SCPs to be applied in a similar fashion as base stacks. You can define your SCP definition in a file named `scp.json` and place it in a folder that represents your Organizational Unit (or OU/AccountName path if you are wanting to apply an account-specific SCP) within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account. + +For example, if you have an account named `my_banking_account` under the `banking/dev` OU that needs a specific SCP, and another SCP defined for the whole `deployment` OU, the folder structure would look like this: + +``` +adf-bootstrap <-- This folder lives in the aws-deployment-framework-bootstrap repository on the master account. +│ +└─── deployment +│ | +│ └─── scp.json +│ +│───banking +│ │ +│ └─── dev +│ │ +│ └─── my_banking_account +│ └─── scp.json +│ +``` +The file `adf-bootstrap/deployment/scp.json` applies the defined SCP to the `deployment` *OU*, while the file `adf-bootstrap/banking/dev/my_backing_account/scp.json` applies the defined SCP to the `my_banking_account` *account*. SCPs are available only in an organization that has [all features enabled](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html). Once you have enabled all features within your Organization, ADF can manage and automate the application and updating process of the SCPs. ## Tagging Policies -Tag Policies are a feature that allows you to define rules on how tags can be used on AWS resources in your accounts in AWS Organizations. You can use Tag Policies to easily adopt a standardized approach for tagging AWS resources. You can define your Tagging Policy definition in a file named `tagging-policy.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account. +Tag Policies are a feature that allows you to define rules on how tags can be used on AWS resources in your accounts in AWS Organizations. You can use Tag Policies to easily adopt a standardized approach for tagging AWS resources. You can define your Tagging Policy definition in a file named `tagging-policy.json` and place it in a folder that represents your Organizational Unit within the `adf-bootstrap` folder from the `aws-deployment-framework-bootstrap` repository on the Master Account. Tagging policies can also be applied to single account using the same approach described above for SCPs. Tag Policies are available only in an organization that has [all features enabled](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html). Once you have enabled all features within your Organization, ADF can manage and automate the application and updating process of the Tag Policies. For more information, see [here](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html). diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/organizations.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/organizations.py index d45860247..91939bdd2 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/organizations.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/organizations.py @@ -59,17 +59,32 @@ def enable_organization_policies(self, policy_type='SERVICE_CONTROL_POLICY'): # def trim_policy_path(policy): return policy[2:] if policy.startswith('//') else policy + @staticmethod + def is_ou_id(ou_id): + return ou_id[0] in ['r','o'] + def get_organization_map(self, org_structure, counter=0): for name, ou_id in org_structure.copy().items(): + # Skip accounts - accounts can't have children + if not Organizations.is_ou_id(ou_id): + continue + # List OUs for organization_id in [organization_id['Id'] for organization_id in paginator(self.client.list_children, **{"ParentId":ou_id, "ChildType":"ORGANIZATIONAL_UNIT"})]: if organization_id in org_structure.values() and counter != 0: continue ou_name = self.describe_ou_name(organization_id) trimmed_path = Organizations.trim_policy_path("{0}/{1}".format(name, ou_name)) org_structure[trimmed_path] = organization_id + # List accounts + for account_id in [account_id['Id'] for account_id in paginator(self.client.list_children, **{"ParentId":ou_id, "ChildType":"ACCOUNT"})]: + if account_id in org_structure.values() and counter != 0: + continue + account_name = self.describe_account_name(account_id) + trimmed_path = Organizations.trim_policy_path("{0}/{1}".format(name, account_name)) + org_structure[trimmed_path] = account_id counter = counter + 1 - # Counter is greater than 4 here is the conditional as organizations cannot have more than 5 levels of nested OUs - return org_structure if counter > 4 else self.get_organization_map(org_structure, counter) + # Counter is greater than 5 here is the conditional as organizations cannot have more than 5 levels of nested OUs + 1 accounts "level" + return org_structure if counter > 5 else self.get_organization_map(org_structure, counter) def update_policy(self, content, policy_id): self.client.update_policy( @@ -169,6 +184,16 @@ def describe_ou_name(self, ou_id): except ClientError as error: raise RootOUIDError("OU is the Root of the Organization") from error + def describe_account_name(self, account_id): + try: + response = self.client.describe_account( + AccountId=account_id + ) + return response['Account']['Name'] + except ClientError as error: + LOGGER.error('Failed to retrieve account name for account ID %s', account_id) + raise error + @staticmethod def determine_ou_path(ou_path, ou_child_name): return '{0}/{1}'.format(ou_path, diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/stubs/stub_organizations.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/stubs/stub_organizations.py index 7f516df44..c72a11a3b 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/stubs/stub_organizations.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/stubs/stub_organizations.py @@ -49,3 +49,16 @@ 'Name': 'some_ou_name' } } + +describe_account = { + 'Account': { + 'Id': 'some_account_id', + 'Arn': 'string', + 'Email': 'some_account_email', + 'Name': 'some_account_name', + 'Status': 'ACTIVE', + 'JoinedMethod': 'INVITED' + # Excluding JoinedTimestamp to avoid + # adding dependency on datetime + } +} diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_organizations.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_organizations.py index 8abd8594b..25767d883 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_organizations.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/python/tests/test_organizations.py @@ -46,6 +46,12 @@ def test_describe_ou_name(cls): assert cls.describe_ou_name('some_ou_id') == 'some_ou_name' +def test_describe_account_name(cls): + cls.client = Mock() + cls.client.describe_account.return_value = stub_organizations.describe_account + assert cls.describe_account_name('some_account_id') == 'some_account_name' + + def test_determine_ou_path(cls): assert cls.determine_ou_path( 'some_path', 'some_ou_name'