From 26b7b31d389cecefd02bbda8fac31042f73e40ac Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Fri, 3 May 2024 11:15:40 -0700 Subject: [PATCH 1/2] Fix account creation when account already exists --- guide/content/deployment-steps.md | 21 +-- .../awsCreateAccount/awsCreateAccount.py | 122 ++++++------------ .../awsCredentialAccount.py | 19 ++- 3 files changed, 66 insertions(+), 96 deletions(-) diff --git a/guide/content/deployment-steps.md b/guide/content/deployment-steps.md index 425a387..9bc8926 100644 --- a/guide/content/deployment-steps.md +++ b/guide/content/deployment-steps.md @@ -26,20 +26,25 @@ description: Deployment steps Wait for the CloudFormation status to change to `CREATE_COMPLETE` state. -## Launch using Customizations for Control Tower or as a Stack Set {#launch-cfct} +## Launch on AWS Organizations member accounts using AWS CloudFormation Stacksets +If you're using this solution in an AWS organization that doesn't use AWS Control Tower, you need to create IAM roles to [Set up basic permissions for stack set operations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html#stacksets-prereqs-accountsetup) so that this ABI solution can be deployed to all member accounts in the AWS Organizations or to specific accounts or OUs you select. + a. You need to create an IAM role (AWSCloudFormationStackSetAdministrationRole) in your management account to establish a trusted relationship between the account you're administering the stack set from and the account you're deploying stack instances to. The CloudFormation template to create this role is [available here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html#stacksets-prereqs-accountsetup). + b. You need to create an IAM execution role (AWSCloudFormationStackSetExecutionRole) for AWS CloudFormation to deploy the StackSets across all member accounts with in the organization. You can use [this CloudFormation template](https://s3.amazonaws.com/cloudformation-stackset-sample-templates-us-east-1/AWSCloudFormationStackSetExecutionRole.yml) and deploy the stack acoss the organization using instructions from [Create a stack set with service-managed permissions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-getting-started-create.html#stacksets-orgs-associate-stackset-with-org) + c. From your Management Account create your AWS CloudFormation StackSets and chose Self-service permissions and use `AWSCloudFormationStackSetExecutionRole` for the IAM admin role name and `AWSCloudFormationStackSetExecutionRole` for the IAM execution role name and then you can select the CloudFormation template from `https://github.com/aws-ia/cfn-abi-spotbynetapp-cloudcheckr/blob/main/templates/CCBuiltIn.yaml`. + [AWS CloudFormation StackSets Self-service permissions](/images/stack-set-admin.png) -You can use CfCT to deploy the templates provided with the AWS Built-in package. -### Prerequisites +## Launch using Customizations for Control Tower (CfCT) {#launch-cfct} + +Customizations for AWS Control Tower combines AWS Control Tower and other highly available, trusted AWS services to help customers set up a secure, multiaccount AWS environment according to AWS best practices. You can add customizations to your AWS Control Tower landing zone using an AWS CloudFormation template and service control policies (SCPs). You can deploy the custom template and policies to individual accounts and organizational units (OUs) within your organization. -The CfCT solution does not launch resources on the management account. Therefore, you must create the role with required permissions in the management account. +CfCT also integrates with AWS Control Tower lifecycle events to hlep ensure that resource deployments stay in sync with your landing zone. For example, when you create a new account using AWS Control Tower account factory, CfCT deploys all of the resources that are attached to the account. -Control Tower permissions: -(/images/control-tower-admin.png) +The templates provided by this ABI package are deployable through CfCT. -StackSet permissions: +### Prerequisites -(/images/stack-set-admin.png) +The CfCT solution can't launch resources in the management account by default. You need select pCreateAWSControlTowerExecutionRole : true to allow the stack to create the role or must manually create a role in that account that has necessary permissions. ### How it works diff --git a/lambda_functions/source/awsCreateAccount/awsCreateAccount.py b/lambda_functions/source/awsCreateAccount/awsCreateAccount.py index 9b1aec0..88aa15a 100644 --- a/lambda_functions/source/awsCreateAccount/awsCreateAccount.py +++ b/lambda_functions/source/awsCreateAccount/awsCreateAccount.py @@ -11,6 +11,9 @@ import threading import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + def lambda_handler(event, context): response = {'accountId': None} timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) @@ -27,31 +30,30 @@ def lambda_handler(event, context): account_aliases, account_number = get_account_name() accountName = account_aliases[0] if account_aliases else account_number bearerToken = get_access_token("https://auth-"+Environment+".cloudcheckr.com/auth/connect/token", APIKey, APISecret) - response = createAccount(customerNumber, accountName, bearerToken, Environment) - if response.get('accountId') is None: - sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + response['body']}) - return { - 'statusCode': 500, - 'body': 'An error occurred during the Lambda execution: ' + response['body'] - } + existingAccountId = check_existing_account(customerNumber, bearerToken, account_number, Environment) + if existingAccountId: + response['accountId'] = existingAccountId + logger.info(f"Account already exists. CloudCheckr Account ID: {existingAccountId}") + else: + response = createAccount(customerNumber, accountName, bearerToken, Environment) + if response.get('accountId') is None: + send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + response['body']}) + return {'statusCode': 500, 'body': 'An error occurred during the Lambda execution: ' + response['body']} except Exception as e: + logger.error(f"Lambda execution error: {e}") timer.cancel() - sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + str(e)}) - return { - 'statusCode': 500, - 'body': 'An error occurred during the Lambda execution: ' + str(e) - } + send_response(event, context, 'FAILED', {'Error': str(e)}) + return {'statusCode': 500, 'body': str(e)} finally: timer.cancel() - sendResponse = send_response(event, context, 'SUCCESS', {'accountNumber': response['accountId']}) + send_response(event, context, 'SUCCESS', {'accountNumber': response['accountId']}) def timeout(event, context): - logging.error('Execution is about to time out, sending failure response to CloudFormation') + logger.error('Execution is about to time out, sending failure response to CloudFormation') send_response(event, context, 'FAILED', {'Error': 'Execution is about to time out'}) - def send_response(event, context, response_status, response_data): response_body = json.dumps({ 'Status': response_status, @@ -72,98 +74,48 @@ def send_response(event, context, response_status, response_data): with urllib.request.urlopen(req) as f: pass -def getPreviousAccountNameID(customer_number, bearer_token, accountName, Environment): - url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts?search="+accountName +def check_existing_account(customer_number, bearer_token, provider_identifier, Environment): + url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts?search="+provider_identifier headers = { - 'Accept': 'text/plain', + 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + bearer_token } try: request = urllib.request.Request(url, headers=headers, method='GET') with urllib.request.urlopen(request, timeout=15) as response: - response_text = response.read().decode() - response_json = json.loads(response_text) - if 'items' in response_json and len(response_json['items']) > 0: - account_id = response_json['items'][0].get('id') - print(f"Account ID found: {account_id}") - return account_id - else: - print("No accounts found matching the search.") - return None - except urllib.error.HTTPError as e: - print(f"HTTP Error while retrieving account ID: {e}") - return None + response_json = json.loads(response.read().decode()) + for item in response_json.get('items', []): + if item.get('providerIdentifier') == provider_identifier: + return item.get('id') + return None except Exception as e: - print(f"Unexpected error: {e}") + logger.error(f"Error checking existing accounts: {e}") return None - def createAccount(customer_number, accountName, bearer_token, Environment): url = f"https://api-"+Environment+".cloudcheckr.com/customer/v1/customers/"+customer_number+"/account-management/accounts" - payload = json.dumps({ - "item": { - "name": accountName, - "provider": "AWS" - } - }) - headers = { - 'Accept': 'text/plain', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + bearer_token - } + payload = json.dumps({"item": {"name": accountName, "provider": "AWS"}}) + headers = {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + bearer_token} try: request = urllib.request.Request(url, data=payload.encode(), headers=headers, method='POST') with urllib.request.urlopen(request, timeout=15) as response: - response_text = response.read().decode() - response_json = json.loads(response_text) - - if 'id' in response_json: - return { - 'statusCode': 200, - 'body': 'completed!', - 'accountId': response_json['id'], - 'bearerToken': bearer_token - } - - return {'statusCode': 200, 'body': 'No ID found, but no errors', 'accountId': None, 'bearerToken': bearer_token} - - except urllib.error.HTTPError as e: - print(f"HTTP Error: {e.code} {e.reason}") - if e.code == 400: - account_id = getPreviousAccountNameID(customer_number, bearer_token, accountName, Environment) - if account_id: - return {'statusCode': 200, 'body': 'Account ID retrieved from existing account', 'accountId': account_id} - else: - return {'statusCode': 500, 'body': 'Failed to retrieve existing account ID', 'accountId': None} - else: - return {'statusCode': e.code, 'body': str(e.reason), 'accountId': None} + response_json = json.loads(response.read().decode()) + return {'statusCode': 200, 'body': 'Account created!', 'accountId': response_json.get('id'), 'bearerToken': bearer_token} except Exception as e: - print(f"Unexpected error: {e}") - return {'statusCode': 500, 'body': 'An unexpected error occurred', 'accountId': None} + logger.error(f"Error creating account: {e}") + return {'statusCode': 500, 'body': str(e), 'accountId': None} def get_access_token(url, client_id, client_secret): auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("utf-8") data = urllib.parse.urlencode({"grant_type": "client_credentials"}).encode("utf-8") - - req = urllib.request.Request( - url, - data=data, - headers={ - "Authorization": f"Basic {auth_header}", - "Content-Type": "application/x-www-form-urlencoded" - }, - method='POST' - ) - + req = urllib.request.Request(url, data=data, headers={"Authorization": f"Basic {auth_header}", "Content-Type": "application/x-www-form-urlencoded"}, method='POST') with urllib.request.urlopen(req) as response: - response_json = json.loads(response.read().decode()) - return response_json["access_token"] + return json.loads(response.read().decode())["access_token"] def get_account_name(): iam = boto3.client('iam') - response = iam.list_account_aliases() + account_aliases = iam.list_account_aliases().get('AccountAliases', []) account_number = boto3.client('sts').get_caller_identity()['Account'] - print(response['AccountAliases'], account_number) - return response['AccountAliases'], account_number - + logger.info(f"Account aliases: {account_aliases}, Account number: {account_number}") + return account_aliases, account_number diff --git a/lambda_functions/source/awsCredentialAccount/awsCredentialAccount.py b/lambda_functions/source/awsCredentialAccount/awsCredentialAccount.py index 238497e..16d7f87 100644 --- a/lambda_functions/source/awsCredentialAccount/awsCredentialAccount.py +++ b/lambda_functions/source/awsCredentialAccount/awsCredentialAccount.py @@ -5,11 +5,14 @@ import json import base64 +import boto3 import urllib.request import urllib.parse import threading import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) def lambda_handler(event, context): response = None @@ -35,7 +38,7 @@ def lambda_handler(event, context): except Exception as e: timer.cancel() - print("Error: ", e) + logger.error(f"Error: {e}") sendResponse = send_response(event, context, 'FAILED', {'Error': 'An error occurred during the Lambda execution: ' + str(e)}) return { 'statusCode': 500, @@ -53,10 +56,14 @@ def timeout(event, context): def credentialAccount(customerNumber, accountNumber, RoleArn, bearerToken, Environment): url = f"https://api-"+Environment+".cloudcheckr.com/credential/v1/customers/"+customerNumber+"/accounts/"+accountNumber+"/credentials/aws" - + aws_partition = get_aws_partition() + if aws_partition == "aws-us-gov": + regionGroup = "GovUs" + else: + regionGroup = "Commercial" payload = json.dumps({ "item": { - "regionGroup": "Commercial", + "regionGroup": regionGroup, "crossAccountRole": { "roleArn": RoleArn } @@ -121,3 +128,9 @@ def get_access_token(url, client_id, client_secret): response = urllib.request.urlopen(request) response_json = json.loads(response.read().decode()) return response_json["access_token"] + +def get_aws_partition(): + session = boto3.session.Session() + region_name = session.region_name + partition = session.get_partition_for_region(region_name) + return partition \ No newline at end of file From a8b9ffd297ec09ab216047ad9c4af9f2c57a1ddf Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Mon, 20 May 2024 11:38:43 -0700 Subject: [PATCH 2/2] Remove noecho on CloudCheckr environment --- templates/CCBuiltIn.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/CCBuiltIn.yaml b/templates/CCBuiltIn.yaml index 5ba6219..ac5b8b2 100644 --- a/templates/CCBuiltIn.yaml +++ b/templates/CCBuiltIn.yaml @@ -19,7 +19,6 @@ Parameters: pEnvironment: Type: String Description: CloudCheckr Environment(US, EU, AU, GOV) - NoEcho: true pCustomerNumber: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ Type: String