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..94f6e3f29 --- /dev/null +++ b/src/lambda_codebase/account_processing/configure_account_regions.py @@ -0,0 +1,103 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +""" +Takes regions that the account is not-opted into and opts into 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 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") + 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") + } + 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: + 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, + 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..1d3b8f0ff --- /dev/null +++ b/src/lambda_codebase/account_processing/tests/test_configure_account_regions.py @@ -0,0 +1,120 @@ +""" +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", + desired_regions=["us-east-1"], + org_root_account_id="123456789", + ) + ) + + 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", + 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/template.yml b/src/template.yml index a5cb8bd5d..bf6430d51 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' @@ -425,6 +427,54 @@ 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" + - "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: @@ -668,7 +718,7 @@ Resources: "Next": "CreateAccount" } ], - "Default": "ConfigureAccountAlias" + "Default": "ConfigureAccountRegions" }, "ConfigureAccountAlias": { "Type": "Task", @@ -745,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",