Skip to content

Commit

Permalink
Feature: Account Management Statemachine will now opt-in to target re…
Browse files Browse the repository at this point in the history
…gions when creating an account (#604)

* Initial commit for enabling regions via the API

* Code Review Changes

* Update src/template.yml

Co-authored-by: Simon Kok

* Fix line length - fixes lint issue

---------

Co-authored-by: Stewart Wallace
Co-authored-by: Simon Kok
Co-authored-by: Javy de Koning
  • Loading branch information
StewartW committed Jul 24, 2023
1 parent c2b82a7 commit 2d50593
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 2 deletions.
103 changes: 103 additions & 0 deletions src/lambda_codebase/account_processing/configure_account_regions.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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",
)
)
88 changes: 86 additions & 2 deletions src/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ Resources:
- !Ref GetAccountRegionsFunctionRole
- !Ref DeleteDefaultVPCFunctionRole
- !Ref AccountAliasConfigFunctionRole
- !Ref AccountRegionConfigFunctionRole
- !Ref AccountTagConfigFunctionRole
- !Ref AccountOUConfigFunctionRole
- !Ref CreateAccountFunctionRole
Expand Down Expand Up @@ -301,6 +302,7 @@ Resources:
- !GetAtt AccountOUConfigFunction.Arn
- !GetAtt GetAccountRegionsFunction.Arn
- !GetAtt DeleteDefaultVPCFunction.Arn
- !GetAtt AccountRegionConfigFunction.Arn

AccountFileProcessingFunction:
Type: 'AWS::Serverless::Function'
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -668,7 +718,7 @@ Resources:
"Next": "CreateAccount"
}
],
"Default": "ConfigureAccountAlias"
"Default": "ConfigureAccountRegions"
},
"ConfigureAccountAlias": {
"Type": "Task",
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 2d50593

Please sign in to comment.