From f51f08f84c69e82d5809317ee8277d7e82df16a7 Mon Sep 17 00:00:00 2001 From: Stewart Wallace Date: Wed, 15 Feb 2023 15:14:23 +0000 Subject: [PATCH 1/4] Initial commit for enabling regions via the API --- requirements.txt | 4 +- .../configure_account_regions.py | 96 +++++++++++++++++++ .../tests/test_configure_account_regions.py | 87 +++++++++++++++++ .../determine_default_branch/requirements.txt | 2 +- .../initial_commit/requirements.txt | 2 +- .../adf-build/requirements.txt | 6 +- .../adf-build/shared/helpers/requirements.txt | 4 +- .../adf-build/shared/requirements.txt | 6 +- .../initial_commit/requirements.txt | 2 +- src/template.yml | 89 ++++++++++++++++- 10 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 src/lambda_codebase/account_processing/configure_account_regions.py create mode 100644 src/lambda_codebase/account_processing/tests/test_configure_account_regions.py diff --git a/requirements.txt b/requirements.txt index 58fc3301f..a12a27bf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ astroid==2.11.7 -boto3==1.26.48 -botocore==1.29.48 +boto3==1.26.71 +botocore>=1.29.71 cfn-lint==0.72.2 docutils==0.16 isort==5.11.4 diff --git a/src/lambda_codebase/account_processing/configure_account_regions.py b/src/lambda_codebase/account_processing/configure_account_regions.py new file mode 100644 index 000000000..aa07fe47f --- /dev/null +++ b/src/lambda_codebase/account_processing/configure_account_regions.py @@ -0,0 +1,96 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Takes regions that the account is not-optend into and opts out of them. + +""" +from ast import literal_eval + + +import boto3 +from aws_xray_sdk.core import patch_all +from logger import configure_logger + +patch_all() +LOGGER = configure_logger(__name__) + + +def get_regions_from_ssm(ssm_client): + regions = ssm_client.get_parameter(Name="target_regions")["Parameter"].get("Value") + regions = literal_eval(regions) + return regions + + +def enable_regions_for_account( + account_client, account_id, desired_regions, target_is_different_account +): + list_region_args = {} + enable_region_args = {} + if target_is_different_account: + list_region_args["AccountId"] = account_id + enable_region_args["AccountId"] = account_id + region_status_response = account_client.list_regions(**list_region_args) + region_status = { + region.get("RegionName"): region.get("RegionOptStatus") + for region in region_status_response.get("Regions") + } + # Currently no built in paginator for list_regions... + # So we have to do this manually. + next_token = region_status_response.get("NextToken") + if next_token: + while next_token: + list_region_args["NextToken"] = next_token + region_status_response = account_client.list_regions(**list_region_args) + next_token = region_status_response.get("NextToken") + region_status = region_status | { + region.get("RegionName"): region.get("RegionOptStatus") + for region in region_status_response.get("Regions") + } + + regions_enabled = {} + for region in desired_regions: + regions_enabled[region] = False + desired_region_status = region_status.get(region.lower()) + if not desired_region_status: + LOGGER.warning("Unable to obtain status of %s, not enabling") + if desired_region_status == "DISABLED": + LOGGER.info("Enabling Region %s because it is currently Disabled", region) + enable_region_args["RegionName"] = region.lower() + account_client.enable_region(**enable_region_args) + else: + LOGGER.info( + "Not enabling Region: %s because it is: %s", + region, + desired_region_status, + ) + if desired_region_status in ["ENABLED_BY_DEFAULT", "ENABLED"]: + regions_enabled[region] = True + LOGGER.info(regions_enabled) + return all(regions_enabled.values()) + + +def lambda_handler(event, _): + desired_regions = [] + if event.get("regions"): + LOGGER.info( + "Account Level Regions is not currently supported. Ignoring these values for now and using SSM only" + ) + desired_regions.extend(get_regions_from_ssm(boto3.client("ssm"))) + org_root_account_id = boto3.client("sts").get_caller_identity().get("Account") + target_account_id = event.get("account_id") + LOGGER.info( + "Target Account Id: %s - This is running in %s. These are the same: %s", + target_account_id, + org_root_account_id, + target_account_id == org_root_account_id, + ) + all_regions_enabled = enable_regions_for_account( + boto3.client("account"), + target_account_id, + desired_regions, + target_account_id != org_root_account_id, + ) + event["all_regions_enabled"] = all_regions_enabled + + return event diff --git a/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py b/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py new file mode 100644 index 000000000..b009170d0 --- /dev/null +++ b/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py @@ -0,0 +1,87 @@ +""" +Tests the account alias configuration lambda +""" + +import unittest +import boto3 +from botocore.stub import Stubber +from aws_xray_sdk import global_sdk_config +from ..configure_account_regions import get_regions_from_ssm, enable_regions_for_account + +global_sdk_config.set_sdk_enabled(False) + + +class SuccessTestCase(unittest.TestCase): + def test_get_regions_from_ssm(self): + ssm_client = boto3.client("ssm", region_name="us-east-1") + ssm_stubber = Stubber(ssm_client) + ssm_stubber.add_response("get_parameter", {"Parameter": {"Value": "[1,2,3]"}}) + ssm_stubber.activate() + self.assertListEqual(get_regions_from_ssm(ssm_client), [1, 2, 3]) + + def test_enable_regions_for_account(self): + accounts_client = boto3.client("account", region_name="us-east-1") + account_stubber = Stubber(accounts_client) + account_stubber.add_response( + "list_regions", + { + "Regions": [ + {"RegionName": "us-east-1", "RegionOptStatus": "ENABLED_BY_DEFAULT"} + ] + }, + ) + account_stubber.activate() + self.assertTrue( + enable_regions_for_account( + accounts_client, + "123456789", + ["us-east-1"], + False, + ) + ) + + def test_enable_regions_for_account_with_pagination(self): + accounts_client = boto3.client("account", region_name="us-east-1") + account_stubber = Stubber(accounts_client) + account_stubber.add_response( + "list_regions", + { + "Regions": [ + {"RegionName": "us-east-1", "RegionOptStatus": "ENABLED_BY_DEFAULT"} + ], + "NextToken": "1", + }, + ) + account_stubber.add_response( + "list_regions", + { + "Regions": [ + {"RegionName": "af-south-1", "RegionOptStatus": "DISABLED"} + ], + "NextToken": "2", + }, + ) + account_stubber.add_response( + "list_regions", + {"Regions": [{"RegionName": "sco-west-1", "RegionOptStatus": "DISABLED"}]}, + ) + account_stubber.add_response( + "enable_region", + {}, + {"RegionName": "af-south-1"}, + ) + account_stubber.add_response( + "enable_region", + {}, + {"RegionName": "sco-west-1"}, + ) + account_stubber.activate() + self.assertFalse( + enable_regions_for_account( + accounts_client, + "123456789", + ["us-east-1", "af-south-1", "sco-west-1"], + False, + ) + ) + account_stubber.assert_no_pending_responses() diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/determine_default_branch/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/determine_default_branch/requirements.txt index 77e7dd2b4..df909fec1 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/determine_default_branch/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/determine_default_branch/requirements.txt @@ -1,2 +1,2 @@ -boto3==1.26.48 +boto3==1.26.71 cfn-custom-resource~=1.0.1 diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/initial_commit/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/initial_commit/requirements.txt index c598ea5b6..37be9e01d 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/initial_commit/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-bootstrap/deployment/lambda_codebase/initial_commit/requirements.txt @@ -1,4 +1,4 @@ Jinja2==3.1.2 -boto3==1.26.48 +boto3==1.26.71 cfn-custom-resource~=1.0.1 markupsafe==2.1.1 diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt index 0f0131d29..80886858d 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt @@ -2,9 +2,9 @@ # account) astroid==2.11.7 aws-sam-cli==1.69.0 -awscli==1.27.48 -boto3==1.26.48 -botocore==1.29.48 +awscli==1.27.71 +boto3==1.26.71 +botocore>=1.29.71 mock~=5.0.1 pip~=22.3.1 pylint~=2.13.9 diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/helpers/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/helpers/requirements.txt index 7524a24d6..f7f1ca3a6 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/helpers/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/helpers/requirements.txt @@ -1,3 +1,3 @@ -boto3==1.26.48 -botocore==1.29.48 +boto3==1.26.71 +botocore>=1.29.71 docopt~=0.6.2 diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/requirements.txt index 0a0413f78..965150df0 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/requirements.txt @@ -50,9 +50,9 @@ aws-cdk.core~=1.188.0 aws-cdk.cx-api~=1.188.0 aws-cdk.region-info~=1.188.0 aws-sam-cli==1.69.0 -awscli==1.27.48 -boto3==1.26.48 -botocore==1.29.48 +awscli==1.27.71 +boto3==1.26.71 +botocore>=1.29.71 mock~=5.0.1 pytest~=7.2.0 pyyaml~=5.4 diff --git a/src/lambda_codebase/initial_commit/requirements.txt b/src/lambda_codebase/initial_commit/requirements.txt index c598ea5b6..37be9e01d 100644 --- a/src/lambda_codebase/initial_commit/requirements.txt +++ b/src/lambda_codebase/initial_commit/requirements.txt @@ -1,4 +1,4 @@ Jinja2==3.1.2 -boto3==1.26.48 +boto3==1.26.71 cfn-custom-resource~=1.0.1 markupsafe==2.1.1 diff --git a/src/template.yml b/src/template.yml index 73f8c49a7..4af2347d7 100644 --- a/src/template.yml +++ b/src/template.yml @@ -263,6 +263,7 @@ Resources: - !Ref GetAccountRegionsFunctionRole - !Ref DeleteDefaultVPCFunctionRole - !Ref AccountAliasConfigFunctionRole + - !Ref AccountRegionConfigFunctionRole - !Ref AccountTagConfigFunctionRole - !Ref AccountOUConfigFunctionRole - !Ref CreateAccountFunctionRole @@ -301,6 +302,7 @@ Resources: - !GetAtt AccountOUConfigFunction.Arn - !GetAtt GetAccountRegionsFunction.Arn - !GetAtt DeleteDefaultVPCFunction.Arn + - !GetAtt AccountRegionConfigFunction.Arn AccountFileProcessingFunction: Type: 'AWS::Serverless::Function' @@ -424,6 +426,55 @@ Resources: FunctionName: AccountTagConfigurationFunction Role: !GetAtt AccountTagConfigFunctionRole.Arn + AccountRegionConfigFunctionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - lambda.amazonaws.com + Action: "sts:AssumeRole" + Path: "/aws-deployment-framework/account-management/" + Policies: + - PolicyName: "adf-lambda-account-region-resource-policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "account:ListRegions" + - "account:EnableRegion" + # - "account:DisableRegion" + - "sts:GetCallerIdentity" + Resource: "*" + - Effect: Allow + Action: ssm:GetParameter + Resource: + - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/target_regions" + + AccountRegionConfigFunction: + Type: 'AWS::Serverless::Function' + Properties: + Handler: configure_account_regions.lambda_handler + Description: "ADF Lambda Function - Account region Configuration" + CodeUri: lambda_codebase/account_processing + Architectures: + - arm64 + Tracing: Active + Layers: + - !Ref LambdaLayerVersion + Environment: + Variables: + MASTER_ACCOUNT_ID: !Ref AWS::AccountId + ORGANIZATION_ID: !GetAtt Organization.OrganizationId + ADF_VERSION: !FindInMap ['Metadata', 'ADF', 'Version'] + ADF_LOG_LEVEL: !Ref LogLevel + FunctionName: AccountRegionConfigurationFunction + Role: !GetAtt AccountRegionConfigFunctionRole.Arn + AccountOUConfigFunction: Type: 'AWS::Serverless::Function' Properties: @@ -667,7 +718,7 @@ Resources: "Next": "CreateAccount" } ], - "Default": "ConfigureAccountAlias" + "Default": "ConfigureAccountRegions" }, "ConfigureAccountAlias": { "Type": "Task", @@ -744,7 +795,41 @@ Resources: "MaxAttempts": 6 } ], - "Next": "ConfigureAccountAlias" + "Next": "ConfigureAccountRegions" + }, + "ConfigureAccountRegions": { + "Type": "Task", + "Resource": "${AccountRegionConfigFunction.Arn}", + "Retry": [ + { + "ErrorEquals": [ + "Lambda.ServiceException", + "Lambda.AWSLambdaException", + "Lambda.SdkClientException", + "Lambda.TooManyRequestsException" + ], + "IntervalSeconds": 2, + "MaxAttempts": 6, + "BackoffRate": 2 + } + ], + "Next": "AreRegionsConfigured" + }, + "AreRegionsConfigured": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.all_regions_enabled", + "BooleanEquals": true, + "Next": "ConfigureAccountAlias" + } + ], + "Default": "Wait 15 seconds" + }, + "Wait 15 seconds": { + "Type": "Wait", + "Seconds": 15, + "Next": "ConfigureAccountRegions" }, "ConfigureAccountTags": { "Type": "Task", From 759ca8eb705de772a9fb7c001ca44b09d7433f1d Mon Sep 17 00:00:00 2001 From: Stewart Wallace Date: Fri, 17 Mar 2023 15:00:29 +0000 Subject: [PATCH 2/4] Code Review Changes --- .../configure_account_regions.py | 28 ++++++++----- .../tests/test_configure_account_regions.py | 41 +++++++++++++++++-- .../adf-build/requirements.txt | 2 +- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/lambda_codebase/account_processing/configure_account_regions.py b/src/lambda_codebase/account_processing/configure_account_regions.py index aa07fe47f..bb3d8ce3a 100644 --- a/src/lambda_codebase/account_processing/configure_account_regions.py +++ b/src/lambda_codebase/account_processing/configure_account_regions.py @@ -2,8 +2,7 @@ # SPDX-License-Identifier: MIT-0 """ -Takes regions that the account is not-optend into and opts out of them. - +Takes regions that the account is not-opted into and opts into them. """ from ast import literal_eval @@ -22,14 +21,7 @@ def get_regions_from_ssm(ssm_client): return regions -def enable_regions_for_account( - account_client, account_id, desired_regions, target_is_different_account -): - list_region_args = {} - enable_region_args = {} - if target_is_different_account: - list_region_args["AccountId"] = account_id - enable_region_args["AccountId"] = account_id +def get_region_status(account_client, **list_region_args): region_status_response = account_client.list_regions(**list_region_args) region_status = { region.get("RegionName"): region.get("RegionOptStatus") @@ -47,6 +39,20 @@ def enable_regions_for_account( region.get("RegionName"): region.get("RegionOptStatus") for region in region_status_response.get("Regions") } + return region_status + + +def enable_regions_for_account( + account_client, account_id, desired_regions, org_root_account_id +): + list_region_args = {} + enable_region_args = {} + target_is_different_account = org_root_account_id != account_id + if target_is_different_account: + list_region_args["AccountId"] = account_id + enable_region_args["AccountId"] = account_id + + region_status = get_region_status(account_client, **list_region_args) regions_enabled = {} for region in desired_regions: @@ -89,7 +95,7 @@ def lambda_handler(event, _): boto3.client("account"), target_account_id, desired_regions, - target_account_id != org_root_account_id, + org_root_account_id, ) event["all_regions_enabled"] = all_regions_enabled diff --git a/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py b/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py index b009170d0..1d3b8f0ff 100644 --- a/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py +++ b/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py @@ -35,8 +35,8 @@ def test_enable_regions_for_account(self): enable_regions_for_account( accounts_client, "123456789", - ["us-east-1"], - False, + desired_regions=["us-east-1"], + org_root_account_id="123456789", ) ) @@ -80,8 +80,41 @@ def test_enable_regions_for_account_with_pagination(self): enable_regions_for_account( accounts_client, "123456789", - ["us-east-1", "af-south-1", "sco-west-1"], - False, + desired_regions=["us-east-1", "af-south-1", "sco-west-1"], + org_root_account_id="123456789", ) ) account_stubber.assert_no_pending_responses() + + def test_enable_regions_for_account_that_is_not_current_account(self): + accounts_client = boto3.client("account", region_name="us-east-1") + account_stubber = Stubber(accounts_client) + account_stubber.add_response( + "list_regions", + { + "Regions": [ + { + "RegionName": "us-east-1", + "RegionOptStatus": "ENABLED_BY_DEFAULT", + }, + {"RegionName": "sco-west-1", "RegionOptStatus": "DISABLED"}, + ] + }, + ) + account_stubber.add_response( + "enable_region", + {}, + { + "RegionName": "sco-west-1", + "AccountId": "123456789", + }, + ) + account_stubber.activate() + self.assertFalse( + enable_regions_for_account( + accounts_client, + "123456789", + desired_regions=["us-east-1", "sco-west-1"], + org_root_account_id="987654321", + ) + ) diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt index 80886858d..8cdcdccaf 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/requirements.txt @@ -4,7 +4,7 @@ astroid==2.11.7 aws-sam-cli==1.69.0 awscli==1.27.71 boto3==1.26.71 -botocore>=1.29.71 +botocore==1.29.71 mock~=5.0.1 pip~=22.3.1 pylint~=2.13.9 From 6ab3610198cd51d4d0f28ae7063a0cac4c8e9a33 Mon Sep 17 00:00:00 2001 From: Stewart Wallace Date: Fri, 17 Mar 2023 15:07:09 +0000 Subject: [PATCH 3/4] Update src/template.yml Co-authored-by: Simon Kok --- src/template.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/template.yml b/src/template.yml index 4af2347d7..1fb9b71b8 100644 --- a/src/template.yml +++ b/src/template.yml @@ -447,7 +447,6 @@ Resources: Action: - "account:ListRegions" - "account:EnableRegion" - # - "account:DisableRegion" - "sts:GetCallerIdentity" Resource: "*" - Effect: Allow From 6960eba633f60ed7e573b3dccf107d1e6e060212 Mon Sep 17 00:00:00 2001 From: Simon Kok Date: Mon, 24 Jul 2023 16:32:09 +0200 Subject: [PATCH 4/4] Fix line length - fixes lint issue --- .../account_processing/configure_account_regions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lambda_codebase/account_processing/configure_account_regions.py b/src/lambda_codebase/account_processing/configure_account_regions.py index bb3d8ce3a..94f6e3f29 100644 --- a/src/lambda_codebase/account_processing/configure_account_regions.py +++ b/src/lambda_codebase/account_processing/configure_account_regions.py @@ -80,7 +80,8 @@ def lambda_handler(event, _): desired_regions = [] if event.get("regions"): LOGGER.info( - "Account Level Regions is not currently supported. Ignoring these values for now and using SSM only" + "Account Level Regions is not currently supported." + "Ignoring these values for now and using SSM only" ) desired_regions.extend(get_regions_from_ssm(boto3.client("ssm"))) org_root_account_id = boto3.client("sts").get_caller_identity().get("Account")