diff --git a/VERSION b/VERSION index 45a1b3f4..26aaba0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.2 +1.2.0 diff --git a/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-create-role/aft_account_provisioning_framework_create_role.py b/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-create-role/aft_account_provisioning_framework_create_role.py index 90c7c1db..53d4a03a 100644 --- a/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-create-role/aft_account_provisioning_framework_create_role.py +++ b/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-create-role/aft_account_provisioning_framework_create_role.py @@ -8,11 +8,12 @@ from boto3.session import Session if TYPE_CHECKING: - from mypy_boto3_iam import IAMClient + from mypy_boto3_iam import IAMClient, IAMServiceResource from mypy_boto3_iam.type_defs import CreateRoleResponseTypeDef else: IAMClient = object CreateRoleResponseTypeDef = object + IAMServiceResource = object logger = utils.get_logger() @@ -77,19 +78,33 @@ def create_aft_execution_role( ) exec_iam_client = ct_execution_session.client("iam") + role_name = role_name.split("/")[-1] + try: - role = exec_iam_client.get_role(RoleName=role_name.split("/")[-1]) - logger.info("Role Exists. Exiting") + role = exec_iam_client.get_role(RoleName=role_name) + logger.info("Role Exists. Updating...") + update_aft_role_trust_policy(session, ct_execution_session, role_name) + set_role_policy( + ct_execution_session=ct_execution_session, + role_name=role_name, + policy_arn="arn:aws:iam::aws:policy/AdministratorAccess", + ) return role["Role"]["Arn"] except exec_iam_client.exceptions.NoSuchEntityException: logger.info("Role not found in account. Creating...") return create_role_in_account(session, ct_execution_session, role_name) -def create_role_in_account( +def update_aft_role_trust_policy( session: Session, ct_execution_session: Session, role_name: str -) -> str: - logger.info("Function Start - create_role_in_account") +) -> None: + assume_role_policy_document = get_aft_trust_policy_document(session) + iam_resource: IAMServiceResource = ct_execution_session.resource("iam") + role = iam_resource.Role(name=role_name) + role.AssumeRolePolicy().update(PolicyDocument=assume_role_policy_document) + + +def get_aft_trust_policy_document(session: Session) -> str: trust_policy_template = os.path.join( os.path.dirname(__file__), "iam/trust-policies/aftmanagement.tpl" ) @@ -99,29 +114,47 @@ def create_role_in_account( with open(trust_policy_template) as trust_policy_file: template = trust_policy_file.read() template = template.replace("{AftManagementAccount}", aft_management_account) - assume_role_policy_document = json.loads(template) + return template + +def create_role_in_account( + session: Session, ct_execution_session: Session, role_name: str +) -> str: + logger.info("Function Start - create_role_in_account") + assume_role_policy_document = get_aft_trust_policy_document(session=session) exec_client: IAMClient = ct_execution_session.client("iam") logger.info("Creating Role") response: CreateRoleResponseTypeDef = exec_client.create_role( RoleName=role_name.split("/")[-1], - AssumeRolePolicyDocument=json.dumps(assume_role_policy_document), + AssumeRolePolicyDocument=assume_role_policy_document, Description="AFT Execution Role", MaxSessionDuration=3600, Tags=[ {"Key": "managed_by", "Value": "AFT"}, ], ) - role = response["Role"]["Arn"] + role_arn = response["Role"]["Arn"] logger.info(response) + set_role_policy( + ct_execution_session=ct_execution_session, + role_name=role_name, + policy_arn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + return role_arn + + +def set_role_policy( + ct_execution_session: Session, role_name: str, policy_arn: str +) -> None: + iam_resource: IAMServiceResource = ct_execution_session.resource("iam") + role = iam_resource.Role(name=role_name) + for policy in role.attached_policies.all(): + role.detach_policy(PolicyArn=policy.arn) logger.info("Attaching Role Policy") - exec_client.attach_role_policy( - RoleName=role_name.split("/")[-1], - PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + role.attach_policy( + PolicyArn=policy_arn, ) - logger.info("Returning role") - logger.info(role) - return role + return None def lambda_handler(event: Dict[str, Any], context: Union[Dict[str, Any], None]) -> str: diff --git a/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-get-account-info/aft_account_provisioning_framework_get_account_info.py b/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-get-account-info/aft_account_provisioning_framework_get_account_info.py index aaf7d4c3..62f2c8b6 100644 --- a/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-get-account-info/aft_account_provisioning_framework_get_account_info.py +++ b/modules/aft-account-provisioning-framework/lambda/aft-account-provisioning-framework-get-account-info/aft_account_provisioning_framework_get_account_info.py @@ -3,7 +3,7 @@ import aft_common.aft_utils as utils import boto3 -from aft_common.account import AftAccountInfo +from aft_common.types import AftAccountInfo from boto3.session import Session logger = utils.get_logger() diff --git a/modules/aft-account-request-framework/iam/role-policies/lambda-account-request-action-trigger.tpl b/modules/aft-account-request-framework/iam/role-policies/lambda-account-request-action-trigger.tpl index 1b694202..2c38e8c8 100644 --- a/modules/aft-account-request-framework/iam/role-policies/lambda-account-request-action-trigger.tpl +++ b/modules/aft-account-request-framework/iam/role-policies/lambda-account-request-action-trigger.tpl @@ -1,6 +1,7 @@ { "Version": "2012-10-17", - "Statement": [{ + "Statement": [ + { "Effect": "Allow", "Action": [ "dynamodb:PutItem", @@ -37,27 +38,36 @@ "arn:aws:ssm:${data_aws_region_aft-management_name}:${data_aws_caller_identity_aft-management_account_id}:parameter/aft/*" ] }, - { - "Effect" : "Allow", - "Action" : [ - "sns:Publish" - ], - "Resource" : [ - "${aws_sns_topic_aft_notifications_arn}", - "${aws_sns_topic_aft_failure_notifications_arn}" - ] - }, - { - "Effect" : "Allow", - "Action" : [ - "kms:GenerateDataKey", - "kms:Encrypt", - "kms:Decrypt" - ], - "Resource" : [ - "${aws_kms_key_aft_arn}", - "arn:aws:kms:${data_aws_region_aft-management_name}:${data_aws_caller_identity_aft-management_account_id}:alias/aws/sns" - ] - } + { + "Effect" : "Allow", + "Action" : [ + "sns:Publish" + ], + "Resource" : [ + "${aws_sns_topic_aft_notifications_arn}", + "${aws_sns_topic_aft_failure_notifications_arn}" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "kms:GenerateDataKey", + "kms:Encrypt", + "kms:Decrypt" + ], + "Resource" : [ + "${aws_kms_key_aft_arn}", + "arn:aws:kms:${data_aws_region_aft-management_name}:${data_aws_caller_identity_aft-management_account_id}:alias/aws/sns" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "sts:AssumeRole" + ], + "Resource" : [ + "arn:aws:iam::${data_aws_caller_identity_aft-management_account_id}:role/AWSAFTAdmin" + ] + } ] } diff --git a/modules/aft-account-request-framework/lambda/aft-account-request-action-trigger/aft_account_request_action_trigger.py b/modules/aft-account-request-framework/lambda/aft-account-request-action-trigger/aft_account_request_action_trigger.py index e32dc48c..9aab56f0 100644 --- a/modules/aft-account-request-framework/lambda/aft-account-request-action-trigger/aft_account_request_action_trigger.py +++ b/modules/aft-account-request-framework/lambda/aft-account-request-action-trigger/aft_account_request_action_trigger.py @@ -4,20 +4,21 @@ import aft_common.aft_utils as utils import boto3 +from aft_common.account import Account +from boto3.session import Session logger = utils.get_logger() def new_account_request(record: Dict[str, Any]) -> bool: - if record["eventName"] == "INSERT": - return True - return False - - -def modify_account_request(record: Dict[str, Any]) -> bool: - if record["eventName"] == "MODIFY": - return True - return False + ct_management_session = utils.get_ct_management_session(aft_mgmt_session=Session()) + account_name = utils.unmarshal_ddb_item(record["dynamodb"]["NewImage"])[ + "control_tower_parameters" + ]["AccountName"] + provisioned_product = Account( + ct_management_session=ct_management_session, account_name=account_name + ).provisioned_product + return provisioned_product is None def delete_account_request(record: Dict[str, Any]) -> bool: @@ -74,52 +75,51 @@ def lambda_handler(event: Dict[str, Any], context: Union[Dict[str, Any], None]) try: logger.info("Lambda_handler Event") logger.info(event) - session = boto3.session.Session() # validate event - if "Records" in event: - event_record = event["Records"][0] - if "eventSource" in event_record: - if event_record["eventSource"] == "aws:dynamodb": - logger.info("DynamoDB Event Record Received") - if new_account_request(event_record): - logger.info("New Account Request Received") - sqs_queue = utils.get_ssm_parameter_value( - session, utils.SSM_PARAM_ACCOUNT_REQUEST_QUEUE - ) - sqs_queue = utils.build_sqs_url(session, sqs_queue) - message = build_sqs_message(event_record) - utils.send_sqs_message(session, sqs_queue, message) - elif modify_account_request(event_record): - if control_tower_param_changed(event_record): - logger.info( - "Control Tower Parameter Update Request Received" - ) - sqs_queue = utils.get_ssm_parameter_value( - session, utils.SSM_PARAM_ACCOUNT_REQUEST_QUEUE - ) - sqs_queue = utils.build_sqs_url(session, sqs_queue) - message = build_sqs_message(event_record) - utils.send_sqs_message(session, sqs_queue, message) - else: - logger.info( - "NON-Control Tower Parameter Update Request Received" - ) - payload = build_aft_account_provisioning_framework_event( - event_record - ) - lambda_name = utils.get_ssm_parameter_value( - session, - utils.SSM_PARAM_AFT_ACCOUNT_PROVISIONING_FRAMEWORK_LAMBDA, - ) - utils.invoke_lambda( - session, lambda_name, json.dumps(payload).encode() - ) - elif delete_account_request(event_record): - logger.info("Delete account request Received") - else: - logger.info("Non Service Catalog Request Received") + if "Records" not in event: + return None + event_record = event["Records"][0] + if "eventSource" not in event_record: + return None + if event_record["eventSource"] != "aws:dynamodb": + return None + + logger.info("DynamoDB Event Record Received") + if delete_account_request(event_record): + # Terraform handles removing the request record from DynamoDB + # AWS does not support automated deletion of accounts + logger.info("Delete account request received") + return None + + new_account = new_account_request(event_record) + if new_account: + logger.info("New account request received") + sqs_queue = utils.get_ssm_parameter_value( + session, utils.SSM_PARAM_ACCOUNT_REQUEST_QUEUE + ) + sqs_queue = utils.build_sqs_url(session, sqs_queue) + message = build_sqs_message(event_record) + utils.send_sqs_message(session, sqs_queue, message) + else: + logger.info("Modify account request received") + if control_tower_param_changed(event_record): + logger.info("Control Tower Parameter Update Request Received") + sqs_queue = utils.get_ssm_parameter_value( + session, utils.SSM_PARAM_ACCOUNT_REQUEST_QUEUE + ) + sqs_queue = utils.build_sqs_url(session, sqs_queue) + message = build_sqs_message(event_record) + utils.send_sqs_message(session, sqs_queue, message) + else: + logger.info("NON-Control Tower Parameter Update Request Received") + payload = build_aft_account_provisioning_framework_event(event_record) + lambda_name = utils.get_ssm_parameter_value( + session, + utils.SSM_PARAM_AFT_ACCOUNT_PROVISIONING_FRAMEWORK_LAMBDA, + ) + utils.invoke_lambda(session, lambda_name, json.dumps(payload).encode()) except Exception as e: message = { diff --git a/modules/aft-account-request-framework/lambda/aft-account-request-processor/aft_account_request_processor.py b/modules/aft-account-request-framework/lambda/aft-account-request-processor/aft_account_request_processor.py index 2acfa41f..0865552d 100644 --- a/modules/aft-account-request-framework/lambda/aft-account-request-processor/aft_account_request_processor.py +++ b/modules/aft-account-request-framework/lambda/aft-account-request-processor/aft_account_request_processor.py @@ -139,6 +139,7 @@ def modify_existing_account( "AccountEmail", ], ) + if ( product_outputs_response["Outputs"][0]["OutputValue"] == request["control_tower_parameters"]["AccountEmail"] diff --git a/sources/aft-lambda-layer/aft_common/account.py b/sources/aft-lambda-layer/aft_common/account.py index 162b13ee..312761e0 100644 --- a/sources/aft-lambda-layer/aft_common/account.py +++ b/sources/aft-lambda-layer/aft_common/account.py @@ -1,15 +1,37 @@ -from typing import Literal, TypedDict - - -class AftAccountInfo(TypedDict): - id: str - email: str - name: str - joined_method: str - joined_date: str - status: str - parent_id: str - parent_type: str - org_name: str - type: Literal["account"] - vendor: Literal["aws"] +from typing import TYPE_CHECKING, List, Optional + +from aft_common.aft_utils import get_logger +from boto3.session import Session + +if TYPE_CHECKING: + from mypy_boto3_servicecatalog import ServiceCatalogClient + from mypy_boto3_servicecatalog.type_defs import ( + DescribeProvisionedProductOutputTypeDef, + ProvisionedProductDetailTypeDef, + ) +else: + ServiceCatalogClient = object + DescribeProvisionedProductOutputTypeDef = object + ProvisionedProductDetailTypeDef = object + +logger = get_logger() + + +class Account: + def __init__(self, ct_management_session: Session, account_name: str) -> None: + self.ct_management_session = ct_management_session + self.account_name = account_name + + @property + def provisioned_product(self) -> Optional[ProvisionedProductDetailTypeDef]: + client: ServiceCatalogClient = self.ct_management_session.client( + "servicecatalog" + ) + try: + response: DescribeProvisionedProductOutputTypeDef = ( + client.describe_provisioned_product(Name=self.account_name) + ) + return response["ProvisionedProductDetail"] + except client.exceptions.ResourceNotFoundException: + logger.debug(f"Account with name {self.account_name} does not exists") + return None diff --git a/sources/aft-lambda-layer/aft_common/aft_utils.py b/sources/aft-lambda-layer/aft_common/aft_utils.py index 5a95b024..4b6249ff 100644 --- a/sources/aft-lambda-layer/aft_common/aft_utils.py +++ b/sources/aft-lambda-layer/aft_common/aft_utils.py @@ -53,7 +53,7 @@ STSClient = object CredentialsTypeDef = object -from aft_common.account import AftAccountInfo +from aft_common.types import AftAccountInfo from .logger import Logger @@ -280,23 +280,27 @@ def get_boto_session(credentials: CredentialsTypeDef) -> Session: ) -def get_ct_management_session(session: Session) -> Session: +def get_ct_management_session(aft_mgmt_session: Session) -> Session: ct_mgmt_account = get_ssm_parameter_value( - session, SSM_PARAM_ACCOUNT_CT_MANAGEMENT_ACCOUNT_ID + aft_mgmt_session, SSM_PARAM_ACCOUNT_CT_MANAGEMENT_ACCOUNT_ID ) - administrator_role = get_ssm_parameter_value(session, SSM_PARAM_AFT_ADMIN_ROLE) - execution_role = get_ssm_parameter_value(session, SSM_PARAM_AFT_EXEC_ROLE) - session_name = get_ssm_parameter_value(session, SSM_PARAM_AFT_SESSION_NAME) + administrator_role = get_ssm_parameter_value( + aft_mgmt_session, SSM_PARAM_AFT_ADMIN_ROLE + ) + execution_role = get_ssm_parameter_value(aft_mgmt_session, SSM_PARAM_AFT_EXEC_ROLE) + session_name = get_ssm_parameter_value(aft_mgmt_session, SSM_PARAM_AFT_SESSION_NAME) # Assume aws-aft-AdministratorRole locally local_creds = get_assume_role_credentials( - session, build_role_arn(session, administrator_role), session_name + aft_mgmt_session, + build_role_arn(aft_mgmt_session, administrator_role), + session_name, ) local_assumed_session = get_boto_session(local_creds) # Assume AWSAFTExecutionRole in CT management ct_mgmt_creds = get_assume_role_credentials( local_assumed_session, - build_role_arn(session, execution_role, ct_mgmt_account), + build_role_arn(aft_mgmt_session, execution_role, ct_mgmt_account), session_name, ) return get_boto_session(ct_mgmt_creds) diff --git a/sources/aft-lambda-layer/aft_common/types.py b/sources/aft-lambda-layer/aft_common/types.py new file mode 100644 index 00000000..162b13ee --- /dev/null +++ b/sources/aft-lambda-layer/aft_common/types.py @@ -0,0 +1,15 @@ +from typing import Literal, TypedDict + + +class AftAccountInfo(TypedDict): + id: str + email: str + name: str + joined_method: str + joined_date: str + status: str + parent_id: str + parent_type: str + org_name: str + type: Literal["account"] + vendor: Literal["aws"]