def base_cognito_user_identity_pool_attach_role(construct, identity_pool, unauth_role_config, auth_role_config): """ Function that attach Roles to an Identity Pool. :param construct: Custom construct that will use this function. From the external construct is usually 'self'. :param identity_pool: :param unauth_role_config: :param auth_role_config: :return: """ base_role_name = identity_pool.identity_pool_name unauth_actions = unauth_role_config["actions"] unauth_resources = unauth_role_config["resources"] auth_actions = auth_role_config["actions"] auth_resources = auth_role_config["resources"] unauth_role = base_cognito_identity_pool_unauth_role( construct, identity_pool, resource_name=base_role_name, actions=unauth_actions, resources=unauth_resources) auth_role = base_cognito_identity_pool_auth_role( construct, identity_pool, resource_name=base_role_name, actions=auth_actions, resources=auth_resources) role_attachment = cognito.CfnIdentityPoolRoleAttachment( construct, id=identity_pool.identity_pool_name + "_roleattach", identity_pool_id=identity_pool.ref, roles={ "unauthenticated": unauth_role.role_arn, "authenticated": auth_role.role_arn }, ) return role_attachment
def setup_identity_pool(self): identity_pool = aws_cognito.CfnIdentityPool( self, "pinpoint_integ_test_android", allow_unauthenticated_identities=True) unauthenticated_role = aws_iam.Role( self, "CognitoDefaultUnauthenticatedRole", assumed_by=aws_iam.FederatedPrincipal( "cognito-identity.amazonaws.com", { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" }, }, "sts:AssumeRoleWithWebIdentity", ), ) unauthenticated_role.add_to_policy( aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["cognito-sync:*", "iot:*"], resources=["*"], )) aws_cognito.CfnIdentityPoolRoleAttachment( self, "DefaultValid", identity_pool_id=identity_pool.ref, roles={"unauthenticated": unauthenticated_role.role_arn}, ) self._parameters_to_save["identity_pool_id"] = identity_pool.ref
def __init__( self, scope: core.Construct, id: str, host_name, cert_arn, zone_id, admin_user: str, admin_password: str, cloud9_instance_size: str, participant_limit: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs) stack = core.Stack.of(self) stack.template_options.description = "Connected Drink Dispenser Workshop" # Static Website props: StaticSiteProps = StaticSiteProps( fqdn=host_name, hosted_zone_id=zone_id, certificate_arn=cert_arn, error_configuration=[ { "error_code": 403, "error_caching_min_ttl": 300, "response_code": 200, "response_page_path": "/index.html", }, { "error_code": 404, "error_caching_min_ttl": 300, "response_code": 200, "response_page_path": "/index.html", }, ], output_name="CDDWebSite", ) cdd_site = StaticSiteConstruct(self, "StaticSite", props) # Custom resource to clean out static website bucket prior to delete # TODO: Move this to the StaticSiteConstruct as option props: CustomResourceProps = CustomResourceProps( name=id + "-CR-S3DeleteObjects", lambda_directory="./lambda_functions/cr_s3_delete", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={"BUCKET_NAME": cdd_site.bucket_name}, ) s3_delete_cr = CustomResourceConstruct(self, "EmptyCddS3Bucket", props) # DependsOn the bucket (we need to delete objects before the bucket is deleted) s3_delete_cr.resource.node.add_dependency(cdd_site.bucket_resource) policy_statement = iam.PolicyStatement() policy_statement.add_actions("s3:GetBucket*") policy_statement.add_actions("s3:GetObject*") policy_statement.add_actions("s3:DeleteObject*") policy_statement.add_actions("s3:List*") policy_statement.add_resources(cdd_site.bucket_resource.bucket_arn) policy_statement.add_resources( f"{cdd_site.bucket_resource.bucket_arn}/*") s3_delete_cr.add_policy_to_role(policy_statement) # IAM Constructs user_group = iam.Group( self, "UserGroup", group_name=id + "-CDDUserGroup", managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "ReadOnlyAccess") ], ) # DynamoDB tables user_db = dynamodb.Table( # UserId as key, user "admin" tracks next available dispenser id # No access to users, RW to Cognito Lambda self, "UserTable", table_name=id + "-UserTable", partition_key={ "name": "userName", "type": dynamodb.AttributeType.STRING }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) dispenser_db = dynamodb.Table( # Dispenser ID and credit amount - RO to users, RW to APIs self, "DispenserTable", table_name=id + "-DispenserTable", partition_key={ "name": "dispenserId", "type": dynamodb.AttributeType.STRING, }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) dispenser_events = dynamodb.Table( # Recorded events from dispenser actions self, "DispenserEvents", table_name=id + "-DispenserEvents", partition_key={ "name": "dispenserId", "type": dynamodb.AttributeType.STRING, }, sort_key={ "name": "timestamp", "type": dynamodb.AttributeType.STRING }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) # Cognito Resources # User pool with phone_number as username props: CognitoUserPoolProps = CognitoUserPoolProps( user_pool_name=id + "-users", client_name=id + "-webclient", auto_verified_attributes=["phone_number"], schema=[ { "name": "group", "attributeDataType": "String", "mutable": True, "required": False, }, { "name": "dispenserId", "attributeDataType": "String", "mutable": True, "required": False, }, ], policies={ "passwordPolicy": { "minimumLength": 6, "requireLowercase": True, "requireNumbers": True, "requireSymbols": False, "requireUppercase": False, } }, ) user_pool = CognitoUserPoolConstruct(self, "UserPool", props) # Role and lambda triggers lambda_cognito_access_role = iam.Role( # Access to IDP calls (for triggers) self, "LambdaCognitoAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["cognito-idp:*"], resources=["*"]), iam.PolicyStatement(actions=["dynamodb:*"], resources=["*"]), ]) ], ) # Triggers for UserPool # Pre-sign-up: triggered when username, password, and phone number submitted lambda_cognito_trigger_pre_signup = lambda_.Function( self, "CogntioTriggerPreSignUp", function_name=id + "-CogntioTriggerPreSignUp", code=lambda_.AssetCode("./lambda_functions/cog_pre_signup"), handler="lambda.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_cognito_access_role, timeout=core.Duration.seconds(6), environment={ "USER_TABLE": user_db.table_name, "PARTICIPANT_LIMIT": participant_limit, }, ) lambda_cognito_trigger_pre_signup.add_permission( "AllowCognitoPreSign", principal=iam.ServicePrincipal("cognito-idp.amazonaws.com"), source_arn=user_pool.user_pool_arn, ) # Post confirmation: triggered after validation code provided lambda_cognito_trigger_post_confirm = lambda_.Function( self, "CogntioTriggerPostConfirm", function_name=id + "-CogntioTriggerPostConfirm", code=lambda_.AssetCode("./lambda_functions/cog_post_confirm"), handler="lambda.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_cognito_access_role, timeout=core.Duration.seconds(6), environment={ "USER_TABLE": user_db.table_name, "PARTICIPANT_LIMIT": participant_limit, }, ) lambda_cognito_trigger_post_confirm.add_permission( "AllowCognitoPostConfirm", principal=iam.ServicePrincipal("cognito-idp.amazonaws.com"), source_arn=user_pool.user_pool_arn, ) # Attach triggers to pool user_pool.user_pool.lambda_config = cognito.CfnUserPool.LambdaConfigProperty( pre_sign_up=lambda_cognito_trigger_pre_signup.function_arn, post_confirmation=lambda_cognito_trigger_post_confirm.function_arn, ) cognito.CfnUserPoolGroup( self, "UserPoolCDDUser", group_name="cdd_user", description="General users of CDD (participants)", user_pool_id=user_pool.user_pool_id, ) cognito.CfnUserPoolGroup( self, "UserPoolCDDAdmin", group_name="cdd_admin", description="CDD administrators", user_pool_id=user_pool.user_pool_id, ) identity_pool = cognito.CfnIdentityPool( self, "IdentityPool", identity_pool_name=id.replace("-", "") + "_idpool", allow_unauthenticated_identities=False, cognito_identity_providers=[{ "clientId": user_pool.client_id, "providerName": user_pool.provider_name, }], ) core.CfnOutput( self, "CognitoIdentityPoolId", export_name="CognitoIdentityPoolId", value=identity_pool.ref, ) # Custom resource to create admin user - cannot do via CFn to set password props: CustomResourceProps = CustomResourceProps( name=id + "-CR-CreateCognitoAdminUser", lambda_directory="./lambda_functions/cr_create_admin_user", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={ "COGNITO_USER_POOL_ID": user_pool.user_pool_id, "COGNITO_CLIENT_ID": user_pool.client_id, "ADMIN_USERNAME": admin_user, "ADMIN_PASSWORD": admin_password, }, ) create_admin_user_cr = CustomResourceConstruct(self, "CreateAdminUser", props) # DependsOn the user pool create_admin_user_cr.resource.node.add_dependency(user_pool) policy_statement = iam.PolicyStatement() policy_statement.add_actions("cognito-idp:SignUp") policy_statement.add_actions("cognito-idp:AdminConfirmSignUp") policy_statement.add_resources("*") create_admin_user_cr.add_policy_to_role(policy_statement) # IAM roles for identity pool auth/unauth cog_unauth_role = iam.Role( self, "cognitoUnauthRole", role_name=f"Cognito_{identity_pool.identity_pool_name}_Unauth_Role", assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), ) cog_unauth_role.attach_inline_policy( iam.Policy( self, "cognitoUnauth", policy_name="cognitoUnauth", statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*" ], resources=["*"], ) ], )) cog_auth_role = iam.Role( self, "cognitoAuthRole", role_name=f"Cognito_{identity_pool.identity_pool_name}_Auth_Role", managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonESCognitoAccess") ], assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), ) cog_auth_role.attach_inline_policy( iam.Policy( self, "cognitoAuth", policy_name="cognitoAuth", statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", "execute-api:*", ], resources=["*"], ), # Provide full access to IoT for the authenticated user # The AWS IoT policy scopes down the access iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ], )) # Finally, attach auth and unauth roles to Identity pool cognito.CfnIdentityPoolRoleAttachment( self, "CDDIdentityPoolRoleAttach", identity_pool_id=identity_pool.ref, roles={ "authenticated": cog_auth_role.role_arn, "unauthenticated": cog_unauth_role.role_arn, }, ) ### Supporting IAM Roles and Policies lambda_full_access_role = iam.Role( # Wide open role for Lambda's to access other services self, "LambdaFullAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["*"], resources=["*"]), ]) ], ) lambda_iot_full_access_role = iam.Role( # Wide open role for Lambda's to access other services self, "LambdaIoTFullAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["dynamodb:*", "iot:*"], resources=["*"]), ]) ], ) lambda_api_app_role = iam.Role( # Role for APIG Lambda functions - make specific per Lambda/method if needed self, "ApiAppRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}", ], ), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) lambda_api_delete_user_role = iam.Role( # Role for APIG Lambda delete user - specific as this has to delete multiple services self, "ApiDeleteUserRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}", ], ), iam.PolicyStatement(actions=["cloud9:DeleteEnvironment"], resources=["*"]), iam.PolicyStatement( actions=[ "iam:DeleteLoginProfile", "iam:ListGroupsForUser", "iam:RemoveUserFromGroup", "iam:DeleteUser", ], resources=["*"], ), iam.PolicyStatement( actions=["cognito-idp:AdminDeleteUser"], resources=["*"]), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) lambda_api_dispense_role = iam.Role( # Role for lambda self, "CommandRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", ], ), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) # IoT Policies iot_policy_dispenser_limited = iot.CfnPolicy( self, "IoTDispenserLimitedPolicy", policy_name=id + "-DispenserLimitedAccess", policy_document={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:client/${{iot:Connection.Thing.ThingName}}" ], "Condition": { "Bool": { "iot:Connection.Thing.IsAttached": [True] } }, }, { "Effect": "Allow", "Action": ["iot:Receive"], "Resource": ["*"] }, { "Effect": "Allow", "Action": ["iot:Subscribe"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/*", f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/${{iot:Certificate.Subject.CommonName}}/cmd/${{iot:Certificate.Subject.CommonName}}", ], }, { "Effect": "Allow", "Action": ["iot:Publish"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topic/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/update", f"arn:aws:iot:{stack.region}:{stack.account}:topic/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/get", f"arn:aws:iot:{stack.region}:{stack.account}:topic/test/${{iot:Certificate.Subject.CommonName}}", f"arn:aws:iot:{stack.region}:{stack.account}:topic/cmd/${{iot:Certificate.Subject.CommonName}}/response", ], }, ], }, ) iot_policy_client = iot.CfnPolicy( self, "IoTClientPolicy", policy_name=id + "-IoTClientAccess", policy_document={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect", "iot:Receive"], "Resource": ["*"], }, { "Effect": "Allow", "Action": ["iot:Subscribe"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/events/*", f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/*/shadow/update/accepted", ], }, ], }, ) ### Lambda Functions # General Lambda Functions NOT associated with APIG lambda_process_events = lambda_.Function( self, "ProcessEvents", function_name=id + "-ProcessEvents", code=lambda_.AssetCode("./lambda_functions/process_events"), handler="process_events.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_iot_full_access_role, timeout=core.Duration.seconds(20), environment={ "EVENT_TABLE": dispenser_events.table_name, "STATUS_TABLE": dispenser_db.table_name, }, ) ## API Lambda functions # Return credit for dispenser api_credit_dispenser_function = lambda_.Function( self, "ApiCreditDispenserFunction", function_name=id + "-ApiCreditDispenserFunction", code=lambda_.AssetCode("./lambda_functions/api_credit_dispenser"), handler="credit_dispenser.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, }, ) # Command api_command_function = lambda_.Function( self, "ApiCommandFunction", function_name=id + "-ApiCommandFunction", code=lambda_.AssetCode("./lambda_functions/api_command"), handler="command.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DispenserTable": dispenser_db.table_name, "EventTable": dispenser_events.table_name, }, ) # Request dispense operation (set shadow or command to dispense) api_dispense_function = lambda_.Function( self, "ApiDispenseFunction", function_name=id + "-ApiDispenseFunction", code=lambda_.AssetCode("./lambda_functions/api_dispense"), handler="dispense.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_dispense_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, }, ) # Request dispense operation (set shadow or command to dispense) api_dispenser_status_function = lambda_.Function( self, "ApiDispenserStatusFunction", function_name=id + "-ApiDispenserStatusFunction", code=lambda_.AssetCode("./lambda_functions/api_dispenser_status"), handler="dispenser_status.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={"DISPENSER_TABLE": dispenser_db.table_name}, ) # Request user details from user table, create resources if needed # NOTE: This uses an overley permissive policy to create the resources needed api_get_resources_function = lambda_.Function( self, "ApiGetResourcesFunction", function_name=id + "-ApiGetResourcesFunction", code=lambda_.AssetCode("./lambda_functions/api_get_resources"), handler="get_resources.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_full_access_role, # Timeout is for user creation: certain tasks such as Cloud9 may take longer timeout=core.Duration.seconds(300), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, "USER_TABLE": user_db.table_name, "USER_PERMISSIONS_GROUP": user_group.group_name, "IOT_POLICY_DISPENSER_LIMITED": iot_policy_dispenser_limited.policy_name, "IOT_POLICY_CLIENT": iot_policy_client.policy_name, "CLOUD9_INSTANCE_SIZE": cloud9_instance_size, }, ) # Request user details from user table api_delete_user_function = lambda_.Function( self, "ApiDeleteUserFunction", function_name=id + "-ApiDeleteUserFunction", code=lambda_.AssetCode("./lambda_functions/api_delete_user"), handler="delete_user.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_delete_user_role, timeout=core.Duration.seconds(28), memory_size=256, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, "USER_TABLE": user_db.table_name, "USER_POOL_ID": user_pool.user_pool_id, }, ) ### API Gateway api = apigateway.RestApi( self, id + "-API", api_key_source_type=apigateway.ApiKeySourceType.HEADER, deploy_options=apigateway.StageOptions(stage_name="prod"), ) core.CfnOutput( self, "APIEndpoint", export_name="APIEndpoint", value= f"https://{api.rest_api_id}.execute-api.{stack.region}.amazonaws.com/prod/", ) # Although / is not used as method, provide OPTIONS for hinting CORS add_cors_options(api.root) # Define Cognito authorizer and attach to gateway cog_authorizer = apigateway.CfnAuthorizer( self, "CognitoAuth", name="CognitoAuthName", rest_api_id=api.rest_api_id, type="COGNITO_USER_POOLS", identity_source="method.request.header.Authorization", provider_arns=[user_pool.user_pool_arn], ) # # Resources (paths) and methods (GET, POST, etc.), for the API api_credit_resource = api.root.add_resource("credit") add_resource_method( api_credit_resource, http_method="GET", integration=apigateway.LambdaIntegration( api_credit_dispenser_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_credit_resource) # command api_command_resource = api.root.add_resource("command") add_resource_method( api_command_resource, http_method="GET", integration=apigateway.LambdaIntegration(api_command_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_command_resource) # Actuate dispenser api_dispense_resource = api.root.add_resource("dispense") add_resource_method( api_dispense_resource, http_method="GET", integration=apigateway.LambdaIntegration(api_dispense_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_dispense_resource) # Return dispenser status (from DynamoDB) api_dispenser_status_resource = api.root.add_resource("status") add_resource_method( api_dispenser_status_resource, http_method="GET", integration=apigateway.LambdaIntegration( api_dispenser_status_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_dispenser_status_resource) # Return user details from User Table api_get_resources_resource = api.root.add_resource("getResources") add_resource_method( api_get_resources_resource, http_method="POST", integration=apigateway.LambdaIntegration( api_get_resources_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_get_resources_resource) # Create a user based on valid token api_delete_user_resource = api.root.add_resource("deleteUser") add_resource_method( api_delete_user_resource, http_method="POST", integration=apigateway.LambdaIntegration(api_delete_user_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_delete_user_resource) # Create policy and reference group iam.Policy( self, "UserPermissionsPolicy", groups=[user_group], policy_name=id + "-UserPermissions", statements=[ # Elevated permissions beyond the ReadOnlyUser # Allow seeing all MQTT messages iam.PolicyStatement( actions=["iot:Subscribe", "iot:Connect", "iot:Receive"], resources=["*"], ), # Allow search indexing iam.PolicyStatement( actions=["iot:SearchIndex"], resources=[ f"arn:aws:iot:{stack.region}:{stack.account}:index/AWS_Things" ], ), # Allow changing of security group ingress on EC2 (Cloud9) to support mapping 443 to iam.PolicyStatement( actions=[ "ec2:AuthorizeSecurityGroupIngress", "ec2:RevokeSecurityGroupIngress", ], resources=[ f"arn:aws:ec2:{stack.region}:{stack.account}:security-group/*" ], ), # DENY access to credentials table iam.PolicyStatement( effect=iam.Effect.DENY, actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}" ], ), # DENY access to S3 overall iam.PolicyStatement(effect=iam.Effect.DENY, actions=["s3:*"], resources=["*"]), ], ) # IoT Constructs # Rule to process shadow events and send to logging iot_rule_log_shadow_events = iot.CfnTopicRule( self, "LogShadowEventsRule", rule_name=id.replace("-", "") + "_LogShadowEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Based on shadow topic and content, process messages via Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, topic() AS topic FROM '$aws/things/+/shadow/update/documents'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow rule to invoke the logging function lambda_process_events.add_permission( "AllowIoTRule1", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_shadow_events.attr_arn, ) # Rule to process generic events and send to logging iot_rule_log_generic_events = iot.CfnTopicRule( self, "LogGenericEventsRule", rule_name=id.replace("-", "") + "_LogGenericEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description="Log generic events, enrich, then send to Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, timestamp() AS ts, topic() AS topic FROM 'events'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow generic_events rule to Invoke the process_events function lambda_process_events.add_permission( "AllowIoTRule2", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_generic_events.attr_arn, ) # Rule to process dispenser specific events and send to logging iot_rule_log_dispenser_events = iot.CfnTopicRule( self, "LogDispenserEventsRule", rule_name=id.replace("-", "") + "_LogDispenserEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Log specific dispenser events, enrich, then send to Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, timestamp() AS ts, topic() AS topic FROM 'events/+'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow log_dispenser_events rule to Invoke the process_events function lambda_process_events.add_permission( "AllowIoTRule3", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_dispenser_events.attr_arn, ) # Rule to process cmd/NNN/response WHERE "command=dispense" iot_rule_command_response_dispense = iot.CfnTopicRule( self, "DispenseCommandResponseRule", rule_name=id.replace("-", "") + "_DispenseCommandResponse", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Invoke Lambda to process dispense commands from dispenser", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, topic() AS topic FROM '$aws/things/+/shadow/update/accepted' WHERE isUndefined(state.reported.response) = False", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=api_dispense_function.function_arn)) ], ), ) # Allow command_response rule to Invoke the dispense function to reconcile outstanding requests api_dispense_function.add_permission( "AllowIoTCommandResponseRule", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_command_response_dispense.attr_arn, ) # Custom resource to delete workshop users - run to clean up any lingering ones # if the admin user didn't clean up. A lot of dependsOn as users are created with bindings # to other resources props: CustomResourceProps = CustomResourceProps( name=id + "-CR-DeleteParticipantUsers", lambda_directory="./lambda_functions/cr_delete_participant_users", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={ # Read user records from UserTable "USER_TABLE": user_db.table_name, # Invoke the api_delete_user function "DELETE_USER_LAMBDA_FUNCTION": api_delete_user_function.function_arn, }, ) delete_participant_users_cr = CustomResourceConstruct( self, "DeleteParticpantUsers", props) # DependsOn the API Delete User Function delete_participant_users_cr.resource.node.add_dependency( api_delete_user_function) # DependsOn the user pool to delete Cognito users delete_participant_users_cr.resource.node.add_dependency(user_pool) # DependsOn the DynamoDB UserTable delete_participant_users_cr.resource.node.add_dependency(user_db) # DependsOn the IoT dispenser and client policies delete_participant_users_cr.resource.node.add_dependency( iot_policy_dispenser_limited) delete_participant_users_cr.resource.node.add_dependency( iot_policy_client) # DependsOn the IoT IAM user group delete_participant_users_cr.resource.node.add_dependency(user_group) # Permissions for function to delete users policy_statement = iam.PolicyStatement() policy_statement.add_actions("dynamodb:*") policy_statement.add_resources( f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}" ) delete_participant_users_cr.add_policy_to_role(policy_statement) policy_statement = iam.PolicyStatement() policy_statement.add_actions("lambda:InvokeFunction") policy_statement.add_resources(api_delete_user_function.function_arn) delete_participant_users_cr.add_policy_to_role(policy_statement)
def _create_identity_pool(self) -> cognito.CfnIdentityPool: provider_name = ( self.user_pool.user_pool_provider_name if hasattr(self.user_pool, "user_pool_provider_name") else f"cognito-idp.{self.context.region}.amazonaws.com/{self.context.user_pool_id}" ) pool = cognito.CfnIdentityPool( scope=self, id="identity-pool", identity_pool_name=self.id.replace("-", "_"), allow_unauthenticated_identities=False, allow_classic_flow=False, cognito_identity_providers=[ cognito.CfnIdentityPool.CognitoIdentityProviderProperty( provider_name=provider_name, client_id=self.user_pool_client.user_pool_client_id, ) ], ) name = f"{self.id}-cognito-authenticated-identity-role" authenticated_role = iam.Role( scope=self, id=name, role_name=name, assumed_by=iam.FederatedPrincipal( federated="cognito-identity.amazonaws.com", conditions={ "StringEquals": {"cognito-identity.amazonaws.com:aud": pool.ref}, "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "authenticated"}, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), inline_policies={ "cognito-default": iam.PolicyDocument( statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", "cognito-identity:*", ], resources=["*"], ) ] ), "team-context-parameter": iam.PolicyDocument( statements=[ iam.PolicyStatement( actions=["ssm:DescribeParameters", "ssm:GetParameters"], resources=[ f"arn:aws:ssm:{self.context.region}:{self.context.account_id}:" f"parameter/orbit/{self.context.name}/teams/*" ], ) ] ), }, ) name = f"{self.id}-cognito-unauthenticated-identity-role" unauthenticated_role = iam.Role( scope=self, id=name, role_name=name, assumed_by=iam.FederatedPrincipal( federated="cognito-identity.amazonaws.com", conditions={ "StringEquals": {"cognito-identity.amazonaws.com:aud": pool.ref}, "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated"}, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), inline_policies={ "cognito-default": iam.PolicyDocument( statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", ], resources=["*"], ) ] ) }, ) cognito.CfnIdentityPoolRoleAttachment( scope=self, id=f"{self.id}-role-attachment", identity_pool_id=pool.ref, roles={ "authenticated": authenticated_role.role_arn, "unauthenticated": unauthenticated_role.role_arn, }, ) return pool
def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # Image Bucket image_bucket = s3.Bucket(self, IMG_BUCKET_NAME, removal_policy=cdk.RemovalPolicy.DESTROY) cdk.CfnOutput(self, "imageBucket", value=image_bucket.bucket_name) image_bucket.add_cors_rule( allowed_methods=[s3.HttpMethods.GET, s3.HttpMethods.PUT], allowed_origins=["*"], allowed_headers=["*"], max_age=3000, ) # Thumbnail Bucket resized_image_bucket = s3.Bucket( self, RESIZED_IMG_BUCKET_NAME, removal_policy=cdk.RemovalPolicy.DESTROY) cdk.CfnOutput(self, "resizedBucket", value=resized_image_bucket.bucket_name) resized_image_bucket.add_cors_rule( allowed_methods=[s3.HttpMethods.GET, s3.HttpMethods.PUT], allowed_origins=["*"], allowed_headers=["*"], max_age=3000, ) # S3 Static bucket for website code web_bucket = s3.Bucket( self, WEBSITE_BUCKET_NAME, website_index_document="index.html", website_error_document="index.html", removal_policy=cdk.RemovalPolicy.DESTROY, # uncomment this and delete the policy statement below to allow public access to our # static website # public_read_access=true ) web_policy_statement = iam.PolicyStatement( actions=["s3:GetObject"], resources=[web_bucket.arn_for_objects("*")], principals=[iam.AnyPrincipal()], conditions={"IpAddress": { "aws:SourceIp": ["139.138.203.36"] }}, ) web_bucket.add_to_resource_policy(web_policy_statement) cdk.CfnOutput(self, "bucketURL", value=web_bucket.bucket_website_domain_name) # Deploy site contents to S3 Bucket s3_dep.BucketDeployment( self, "DeployWebsite", sources=[s3_dep.Source.asset("./public")], destination_bucket=web_bucket, ) # DynamoDB to store image labels partition_key = dynamodb.Attribute(name="image", type=dynamodb.AttributeType.STRING) table = dynamodb.Table( self, "ImageLabels", partition_key=partition_key, removal_policy=cdk.RemovalPolicy.DESTROY, ) cdk.CfnOutput(self, "ddbTable", value=table.table_name) # Lambda layer for Pillow library layer = lb.LayerVersion( self, "pil", code=lb.Code.from_asset("reklayer"), compatible_runtimes=[lb.Runtime.PYTHON_3_7], license="Apache-2.0", description= "A layer to enable the PIL library in our Rekognition Lambda", ) # Lambda function rek_fn = lb.Function( self, "rekognitionFunction", code=lb.Code.from_asset("rekognitionFunction"), runtime=lb.Runtime.PYTHON_3_7, handler="index.handler", timeout=cdk.Duration.seconds(30), memory_size=1024, layers=[layer], environment={ "TABLE": table.table_name, "BUCKET": image_bucket.bucket_name, "THUMBBUCKET": resized_image_bucket.bucket_name, }, ) image_bucket.grant_read(rek_fn) resized_image_bucket.grant_write(rek_fn) table.grant_write_data(rek_fn) rek_fn.add_to_role_policy( iam.PolicyStatement(effect=iam.Effect.ALLOW, actions=["rekognition:DetectLabels"], resources=["*"])) # Lambda for Synchronous front end serviceFn = lb.Function( self, "serviceFunction", code=lb.Code.from_asset("servicelambda"), runtime=lb.Runtime.PYTHON_3_7, handler="index.handler", environment={ "TABLE": table.table_name, "BUCKET": image_bucket.bucket_name, "RESIZEDBUCKET": resized_image_bucket.bucket_name, }, ) image_bucket.grant_write(serviceFn) resized_image_bucket.grant_write(serviceFn) table.grant_read_write_data(serviceFn) # Cognito User Pool Auth auto_verified_attrs = cognito.AutoVerifiedAttrs(email=True) sign_in_aliases = cognito.SignInAliases(email=True, username=True) user_pool = cognito.UserPool( self, "UserPool", self_sign_up_enabled=True, auto_verify=auto_verified_attrs, sign_in_aliases=sign_in_aliases, ) user_pool_client = cognito.UserPoolClient(self, "UserPoolClient", user_pool=user_pool, generate_secret=False) identity_pool = cognito.CfnIdentityPool( self, "ImageRekognitionIdentityPool", allow_unauthenticated_identities=False, cognito_identity_providers=[{ "clientId": user_pool_client.user_pool_client_id, "providerName": user_pool.user_pool_provider_name, }], ) # API Gateway cors_options = apigw.CorsOptions(allow_origins=apigw.Cors.ALL_ORIGINS, allow_methods=apigw.Cors.ALL_METHODS) api = apigw.LambdaRestApi( self, "imageAPI", default_cors_preflight_options=cors_options, handler=serviceFn, proxy=False, ) auth = apigw.CfnAuthorizer( self, "ApiGatewayAuthorizer", name="customer-authorizer", identity_source="method.request.header.Authorization", provider_arns=[user_pool.user_pool_arn], rest_api_id=api.rest_api_id, # type=apigw.AuthorizationType.COGNITO, type="COGNITO_USER_POOLS", ) assumed_by = iam.FederatedPrincipal( "cognito-identity.amazon.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ) authenticated_role = iam.Role( self, "ImageRekognitionAuthenticatedRole", assumed_by=assumed_by, ) # IAM policy granting users permission to get and put their pictures policy_statement = iam.PolicyStatement( actions=["s3:GetObject", "s3:PutObject"], effect=iam.Effect.ALLOW, resources=[ image_bucket.bucket_arn + "/private/${cognito-identity.amazonaws.com:sub}/*", image_bucket.bucket_arn + "/private/${cognito-identity.amazonaws.com:sub}/", resized_image_bucket.bucket_arn + "/private/${cognito-identity.amazonaws.com:sub}/*", resized_image_bucket.bucket_arn + "/private/${cognito-identity.amazonaws.com:sub}/", ], ) # IAM policy granting users permission to list their pictures list_policy_statement = iam.PolicyStatement( actions=["s3:ListBucket"], effect=iam.Effect.ALLOW, resources=[ image_bucket.bucket_arn, resized_image_bucket.bucket_arn ], conditions={ "StringLike": { "s3:prefix": ["private/${cognito-identity.amazonaws.com:sub}/*"] } }, ) authenticated_role.add_to_policy(policy_statement) authenticated_role.add_to_policy(list_policy_statement) # Attach role to our Identity Pool cognito.CfnIdentityPoolRoleAttachment( self, "IdentityPoolRoleAttachment", identity_pool_id=identity_pool.ref, roles={"authenticated": authenticated_role.role_arn}, ) # Get some outputs from cognito cdk.CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) cdk.CfnOutput(self, "AppClientId", value=user_pool_client.user_pool_client_id) cdk.CfnOutput(self, "IdentityPoolId", value=identity_pool.ref) # New Amazon API Gateway with AWS Lambda Integration success_response = apigw.IntegrationResponse( status_code="200", response_parameters={ "method.response.header.Access-Control-Allow-Origin": "'*'" }, ) error_response = apigw.IntegrationResponse( selection_pattern="(\n|.)+", status_code="500", response_parameters={ "method.response.header.Access-Control-Allow-Origin": "'*'" }, ) request_template = json.dumps({ "action": "$util.escapeJavaScript($input.params('action'))", "key": "$util.escapeJavaScript($input.params('key'))", }) lambda_integration = apigw.LambdaIntegration( serviceFn, proxy=False, request_parameters={ "integration.request.querystring.action": "method.request.querystring.action", "integration.request.querystring.key": "method.request.querystring.key", }, request_templates={"application/json": request_template}, passthrough_behavior=apigw.PassthroughBehavior.WHEN_NO_TEMPLATES, integration_responses=[success_response, error_response], ) imageAPI = api.root.add_resource("images") success_resp = apigw.MethodResponse( status_code="200", response_parameters={ "method.response.header.Access-Control-Allow-Origin": True }, ) error_resp = apigw.MethodResponse( status_code="500", response_parameters={ "method.response.header.Access-Control-Allow-Origin": True }, ) # GET /images get_method = imageAPI.add_method( "GET", lambda_integration, authorization_type=apigw.AuthorizationType.COGNITO, request_parameters={ "method.request.querystring.action": True, "method.request.querystring.key": True, }, method_responses=[success_resp, error_resp], ) # DELETE /images delete_method = imageAPI.add_method( "DELETE", lambda_integration, authorization_type=apigw.AuthorizationType.COGNITO, request_parameters={ "method.request.querystring.action": True, "method.request.querystring.key": True, }, method_responses=[success_resp, error_resp], ) # Override the authorizer id because it doesn't work when defininting it as a param # in add_method get_method_resource = get_method.node.find_child("Resource") get_method_resource.add_property_override("AuthorizerId", auth.ref) delete_method_resource = delete_method.node.find_child("Resource") delete_method_resource.add_property_override("AuthorizerId", auth.ref) # Building SQS queue and DeadLetter Queue dl_queue = sqs.Queue( self, "ImageDLQueue", queue_name="ImageDLQueue", ) dl_queue_opts = sqs.DeadLetterQueue(max_receive_count=2, queue=dl_queue) queue = sqs.Queue( self, "ImageQueue", queue_name="ImageQueue", visibility_timeout=cdk.Duration.seconds(30), receive_message_wait_time=cdk.Duration.seconds(20), dead_letter_queue=dl_queue_opts, ) # S3 Bucket Create Notification to SQS # Whenever an image is uploaded add it to the queue image_bucket.add_object_created_notification( s3n.SqsDestination(queue), s3.NotificationKeyFilter(prefix="private/"))
def add_cognito(self): password_policy = _cognito.PasswordPolicy( require_lowercase=False, require_digits=False, require_symbols=False, require_uppercase=False, ) user_pool = _cognito.UserPool( self, 'UserPool', password_policy=password_policy, user_pool_name='UserPool', self_sign_up_enabled=True, user_verification={ "email_subject": "Verify your email for our awesome app!", "email_body": "Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}", "email_style": _cognito.VerificationEmailStyle.CODE, }, user_invitation={ "email_subject": "Invite to join our awesome app!", "email_body": "Hello {username}, you have been invited to join our awesome app! Your temporary password is {####}", }, sign_in_aliases={"email": True}, auto_verify={"email": True}, ) user_pool_client = user_pool.add_client("AppClient", auth_flows={ "user_password": True, "user_srp": True, "refresh_token": True, "admin_user_password": True }) idp = _cognito.CfnIdentityPool.CognitoIdentityProviderProperty( client_id=user_pool_client.user_pool_client_id, provider_name=user_pool.user_pool_provider_name) identity_pool = _cognito.CfnIdentityPool( self, "IdPool", allow_unauthenticated_identities=False, cognito_identity_providers=[idp]) authenticated_principal = _iam.FederatedPrincipal( 'cognito-identity.amazonaws.com', { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, "sts:AssumeRoleWithWebIdentity") unauthenticated_principal = _iam.FederatedPrincipal( 'cognito-identity.amazonaws.com', { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, "sts:AssumeRoleWithWebIdentity") authenticated_role = _iam.Role(self, "CognitoDefaultAuthenticatedRole", assumed_by=authenticated_principal) unauthenticated_role = _iam.Role(self, "CognitoDefaultUnAuthenticatedRole", assumed_by=unauthenticated_principal) authenticated_policy = _iam.PolicyStatement( effect=_iam.Effect.ALLOW, actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", "cognito-identity:*" ], resources=["*"]) unauthenticated_policy = _iam.PolicyStatement( effect=_iam.Effect.ALLOW, actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", ], resources=["*"]) authenticated_role.add_to_policy(authenticated_policy) unauthenticated_role.add_to_policy(unauthenticated_policy) _cognito.CfnIdentityPoolRoleAttachment(self, "DefaultValidRoleAttachment", identity_pool_id=identity_pool.ref, roles={ "authenticated": authenticated_role.role_arn, "unauthenticated": unauthenticated_role.role_arn }) return user_pool, identity_pool, user_pool_client
def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None: super().__init__(scope, id, **kwargs) power_transformers = aws_dynamodb.Table( self, "PowerTransformers", table_name="PowerTransformers", partition_key=aws_dynamodb.Attribute( name="name", type=aws_dynamodb.AttributeType.STRING), removal_policy=core.RemovalPolicy.DESTROY) function = _lambda.Function( self, "power_transformers_data_enrichment", function_name="power_transformers_data_enrichment", runtime=_lambda.Runtime.PYTHON_3_7, handler="lambda_function.handler", code=_lambda.Code.asset("./lambda/data-enrichment")) function.add_environment('TABLE_NAME', power_transformers.table_name) function.add_to_role_policy( iam.PolicyStatement(actions=['dynamodb:GetItem'], resources=[f"{power_transformers.table_arn}"], effect=iam.Effect.ALLOW)) function.add_permission( principal=iam.ServicePrincipal('iotanalytics.amazonaws.com'), action='lambda:InvokeFunction', id='pt-iot-analytics') bucket = s3.Bucket( self, 'PowerTransformersTelemetryBucket', bucket_name=f"{props['projectName'].lower()}-{core.Aws.ACCOUNT_ID}", removal_policy=core.RemovalPolicy.DESTROY) output_bucket = s3.Bucket( self, 'PowerTransformersProcessedDataBucket', bucket_name= f"{props['projectName'].lower()}-output-{core.Aws.ACCOUNT_ID}", removal_policy=core.RemovalPolicy.DESTROY) # Apply least privilege s3_role = iam.Role( self, "IotAnalyticsS3Role", assumed_by=iam.ServicePrincipal("iotanalytics.amazonaws.com"), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( 'AmazonS3FullAccess') ]) # s3_role.add_to_policy(iam.PolicyStatement(actions=["s3:PutObject", "s3:DeleteObject", "s3:GetBucketLocation"], # resources=[f"{bucket.bucket_arn}", f"{bucket.bucket_arn}/*"], effect=iam.Effect.ALLOW)) # Apply least privilege s3_output_role = iam.Role( self, "IotAnalyticsS3OutputRole", assumed_by=iam.ServicePrincipal("iotanalytics.amazonaws.com"), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( 'AmazonS3FullAccess') ], ) # s3_output_role.add_to_policy(iam.PolicyStatement(actions=["s3:PutObject", "s3:DeleteObject", "s3:GetBucketLocation"], # resources=[f"{output_bucket.bucket_arn}", f"{output_bucket.bucket_arn}/*"], effect=iam.Effect.ALLOW)) project_name = props['projectName'].lower().replace('-', '_') channel_name = f"{project_name}_channel" datastore_name = f"{project_name}_datastore" channel_s3 = CHANNEL.CustomerManagedS3Property( bucket=bucket.bucket_name, key_prefix='raw/', role_arn=s3_role.role_arn) channel_storage = CHANNEL.ChannelStorageProperty( customer_managed_s3=channel_s3) CHANNEL(self, 'iot_channel', channel_name=channel_name, channel_storage=channel_storage) datastore_s3 = DATASTORE.CustomerManagedS3Property( bucket=bucket.bucket_name, key_prefix='processed/', role_arn=s3_role.role_arn) datastore_storage = DATASTORE.DatastoreStorageProperty( customer_managed_s3=datastore_s3) datastore = DATASTORE(self, 'iot_datastore', datastore_name=datastore_name, datastore_storage=datastore_storage) channel_activity = PIPELINE.ChannelProperty(name='ChannelActivity', channel_name=channel_name, next='LambdaActivity') lambda_activity = PIPELINE.LambdaProperty( name='LambdaActivity', lambda_name='power_transformers_data_enrichment', next='DatastoreActivity', batch_size=10) datastore_activity = PIPELINE.DatastoreProperty( name='DatastoreActivity', datastore_name=datastore_name) pipeline_activities = PIPELINE.ActivityProperty( channel=channel_activity, lambda_=lambda_activity, datastore=datastore_activity) pipeline = PIPELINE(self, 'iot_pipeline', pipeline_name=f"{project_name}_pipeline", pipeline_activities=[pipeline_activities]) pipeline.add_depends_on(datastore) query_action = DATASET.QueryActionProperty( sql_query=f"SELECT * FROM {datastore_name}") action = DATASET.ActionProperty(query_action=query_action, action_name='sqlAction') schedule_expression = DATASET.ScheduleProperty( schedule_expression='cron(1/5 * * * ? *)') trigger_schedule = DATASET.TriggerProperty( schedule=schedule_expression) dataset_s3_destination = DATASET.S3DestinationConfigurationProperty( bucket=output_bucket.bucket_name, key= 'dataset/Version/!{iotanalytics:scheduleTime}_!{iotanalytics:versionId}.csv', role_arn=s3_output_role.role_arn) dataset_destination = DATASET.DatasetContentDeliveryRuleDestinationProperty( s3_destination_configuration=dataset_s3_destination) content_delivery_rules = DATASET.DatasetContentDeliveryRuleProperty( destination=dataset_destination) dataset = DATASET(self, 'iot_dataset', dataset_name=f"{project_name}_dataset", actions=[action], triggers=[trigger_schedule], content_delivery_rules=[content_delivery_rules]) dataset.add_depends_on(datastore) user_pool = aws_cognito.UserPool( self, 'kibanaUserPool', self_sign_up_enabled=False, sign_in_aliases=aws_cognito.SignInAliases(username=True, email=True)) aws_cognito.CfnUserPoolDomain( self, 'userPoolDomain', user_pool_id=user_pool.user_pool_id, domain= f"{props['projectName'].lower()}-{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" ) user_pool_client = aws_cognito.UserPoolClient(self, 'kibanaClientId', user_pool=user_pool, generate_secret=True) identity_provider = aws_cognito.CfnIdentityPool.CognitoIdentityProviderProperty( client_id=user_pool_client.user_pool_client_id, provider_name=user_pool.user_pool_provider_name) identity_pool = aws_cognito.CfnIdentityPool( self, 'identityPool', allow_unauthenticated_identities=False, cognito_identity_providers=[identity_provider]) # Apply least privilege cognito_authenticated_role = iam.Role( self, "CognitoAuthRole", assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", assume_role_action='sts:AssumeRoleWithWebIdentity', conditions={ 'StringEquals': { 'cognito-identity.amazonaws.com:aud': identity_pool.ref }, 'ForAnyValue:StringLike': { 'cognito-identity.amazonaws.com:amr': 'authenticated' } }), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( 'AmazonESFullAccess') ]) aws_cognito.CfnIdentityPoolRoleAttachment( self, 'identityPoolRoleAttachment', identity_pool_id=identity_pool.ref, roles={'authenticated': cognito_authenticated_role.role_arn}) cognito_options = DOMAIN.CognitoOptionsProperty( enabled=True, user_pool_id=user_pool.user_pool_id, identity_pool_id=identity_pool.ref, role_arn= f"arn:aws:iam::{core.Aws.ACCOUNT_ID}:role/service-role/CognitoAccessForAmazonES" ) ebs_options = DOMAIN.EBSOptionsProperty(ebs_enabled=True, volume_size=10, volume_type='gp2') elasticsearch_cluster_config = DOMAIN.ElasticsearchClusterConfigProperty( instance_count=1, instance_type='r5.large.elasticsearch') encryption_at_rest_options = DOMAIN.EncryptionAtRestOptionsProperty( enabled=True) node_to_node_encryption_options = DOMAIN.NodeToNodeEncryptionOptionsProperty( enabled=True) snapshot_options = DOMAIN.SnapshotOptionsProperty( automated_snapshot_start_hour=0) es_domain_arn = f"arn:aws:es:{core.Aws.REGION}:{core.Aws.ACCOUNT_ID}:domain/{props['projectName'].lower()}/*" es_policy_statement = iam.PolicyStatement(actions=['es:*'], resources=[es_domain_arn]) es_policy_statement.add_arn_principal( cognito_authenticated_role.role_arn) policy_document = iam.PolicyDocument() policy_document.add_statements(es_policy_statement) domain = DOMAIN( self, 'elasticsearch', domain_name=f"{props['projectName'].lower()}", cognito_options=cognito_options, ebs_options=ebs_options, elasticsearch_cluster_config=elasticsearch_cluster_config, encryption_at_rest_options=encryption_at_rest_options, node_to_node_encryption_options=node_to_node_encryption_options, snapshot_options=snapshot_options, elasticsearch_version='6.8', access_policies=policy_document) function = _lambda.Function( self, "load_data_from_s3_to_es", function_name="load_data_from_s3_to_es", runtime=_lambda.Runtime.PYTHON_3_7, handler="lambda_function.handler", code=_lambda.Code.asset("./lambda/load-data-from-s3-to-es.zip")) function.add_environment('ES_HOST', domain.attr_domain_endpoint) function.add_environment('ES_REGION', f"{core.Aws.REGION}") function.add_to_role_policy( iam.PolicyStatement(actions=['es:ESHttpPost'], resources=[es_domain_arn], effect=iam.Effect.ALLOW)) function.add_to_role_policy( iam.PolicyStatement(actions=['s3:GetObject'], resources=[f"{output_bucket.bucket_arn}/*"], effect=iam.Effect.ALLOW)) notification = aws_s3_notifications.LambdaDestination(function) output_bucket.add_event_notification(s3.EventType.OBJECT_CREATED, notification) load_ddb_custom_resource = LoadDDBDataCustomResource( self, "LoadDDBData", table_name=power_transformers.table_name, table_arn=power_transformers.table_arn) load_ddb_custom_resource.node.add_dependency(power_transformers) load_es_index_custom_resource = LoadESIndexCustomResource( self, "LoadESIndex", es_host=domain.attr_domain_endpoint, es_region=f"{core.Aws.REGION}", es_domain_arn=es_domain_arn) load_es_index_custom_resource.node.add_dependency(domain) load_kibana_dashboards_custom_resource = LoadKibanaDashboardsCustomResource( self, "LoadKibanaDashboards", es_host=domain.attr_domain_endpoint, es_region=f"{core.Aws.REGION}", es_domain_arn=es_domain_arn) load_kibana_dashboards_custom_resource.node.add_dependency( load_es_index_custom_resource)
def __init__(self, scope: core.Construct, id: str, instance_id: str, contact_flow_id: str, source_phone_number: str, timeout: int, **kwargs): super().__init__(scope, id, **kwargs) web_bucket = _s3.Bucket(self, "StaticWebBucket", website_index_document="index.html", website_error_document="index.html", removal_policy=core.RemovalPolicy.DESTROY, public_read_access=True) core.CfnOutput(self, 'WebBucketUrl', value=web_bucket.bucket_domain_name) web_distribution = _clf.CloudFrontWebDistribution( self, 'StaticWebDistribution', origin_configs=[ _clf.SourceConfiguration( s3_origin_source=_clf.S3OriginConfig( s3_bucket_source=web_bucket), behaviors=[_clf.Behavior(is_default_behavior=True)]) ], viewer_protocol_policy=_clf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS) _s3d.BucketDeployment( self, "S3StaticWebContentDeploymentWithInvalidation", sources=[ _s3d.Source.asset( f"{pathlib.Path(__file__).parent.absolute()}/site-content/build" ) ], destination_bucket=web_bucket, distribution=web_distribution, distribution_paths=["/*"]) file_bucket = _s3.Bucket(self, "FileBucket", removal_policy=core.RemovalPolicy.DESTROY) call_dead_letter_queue = _sqs.Queue(self, "CallDeadLetterQueue", fifo=True, content_based_deduplication=True) call_sqs_queue = _sqs.Queue( self, "CallSqsQueue", fifo=True, content_based_deduplication=True, visibility_timeout=core.Duration.seconds(120), dead_letter_queue=_sqs.DeadLetterQueue( max_receive_count=1, queue=call_dead_letter_queue)) async_call_dead_letter_queue = _sqs.Queue( self, "AsyncCallDeadLetterQueue", fifo=True, content_based_deduplication=True) async_callout_queue = _sqs.Queue( self, "AsyncCalloutQueue", fifo=True, content_based_deduplication=True, visibility_timeout=core.Duration.seconds(120), dead_letter_queue=_sqs.DeadLetterQueue( max_receive_count=1, queue=async_call_dead_letter_queue)) call_job_complete_sns_topic = _sns.Topic( self, "CallJobCompleteSnsTopic", display_name="CallJobCompletion") call_result_table = _dynamodb.Table( self, "CallResultDynamodbTable", billing_mode=_dynamodb.BillingMode.PAY_PER_REQUEST, partition_key=_dynamodb.Attribute( name="task_id", type=_dynamodb.AttributeType.STRING), sort_key=_dynamodb.Attribute(name="receiver_id", type=_dynamodb.AttributeType.STRING), removal_policy=core.RemovalPolicy.DESTROY) callout_record_table = _dynamodb.Table( self, "CallTaskDynamodbTable", billing_mode=_dynamodb.BillingMode.PAY_PER_REQUEST, partition_key=_dynamodb.Attribute( name="task_id", type=_dynamodb.AttributeType.STRING), sort_key=_dynamodb.Attribute(name="created_at", type=_dynamodb.AttributeType.NUMBER), removal_policy=core.RemovalPolicy.DESTROY) callout_record_table.add_global_secondary_index( partition_key=_dynamodb.Attribute( name='call_type', type=_dynamodb.AttributeType.STRING), sort_key=_dynamodb.Attribute(name='created_at', type=_dynamodb.AttributeType.NUMBER), index_name='CallTypeCreatedAtGlobalIndex', projection_type=_dynamodb.ProjectionType.ALL) python_function_layer = _lambda.LayerVersion( self, "LambdaPythonFunctionLayer", code=_lambda.Code.asset("aws_callouts_cdk/layer/_python"), compatible_runtimes=[ _lambda.Runtime.PYTHON_3_7, _lambda.Runtime.PYTHON_3_8 ], license="Available under the MIT-0 license") nodejs_function_layer = _lambda.LayerVersion( self, "LambdaNodeJsFunctionLayer", code=_lambda.Code.asset("aws_callouts_cdk/layer/_nodejs"), compatible_runtimes=[ _lambda.Runtime.NODEJS_10_X, _lambda.Runtime.NODEJS_12_X ], license="Available under the MIT-0 license") global_python_function_arguments = { "code": _lambda.Code.asset("aws_callouts_cdk/src/python"), "layers": [python_function_layer], "runtime": _lambda.Runtime.PYTHON_3_7 } global_nodeje_function_arguments = { "code": _lambda.Code.asset("aws_callouts_cdk/src/nodejs"), "layers": [nodejs_function_layer], "runtime": _lambda.Runtime.NODEJS_12_X } get_callout_job_function = _lambda.Function( self, "GetCalloutJobFunction", handler="get_call_job.lambda_handler", **global_python_function_arguments) get_callout_job_function.add_environment(key="S3Bucket", value=file_bucket.bucket_name) file_bucket.grant_read(get_callout_job_function) callout_function = _lambda.Function(self, "CalloutFunction", handler="send_call.lambda_handler", **global_python_function_arguments) callout_function.add_environment( key="ContactFlowArn", value= f"arn:aws:connect:{self.region}:{self.account}:instance/{instance_id}/contact-flow/{contact_flow_id}" ) callout_function.add_environment(key="SourcePhoneNumber", value=source_phone_number) callout_function.add_environment(key="ExcelFileBucket", value=file_bucket.bucket_name) callout_function.add_environment(key="AsynCalloutQueueUrl", value=async_callout_queue.queue_url) callout_function.add_to_role_policy(statement=_iam.PolicyStatement( resources=[ f"arn:aws:connect:{self.region}:{self.account}:instance/{instance_id}/*" ], actions=["connect:StartOutboundVoiceContact"])) callout_function.add_event_source(source=_les.SqsEventSource( queue=async_callout_queue, batch_size=1)) 'arn:aws:connect:751225572132:ap-southeast-2:instance/9d0c7cc5-7d2a-42e4-a3dd-70f402e0d040' file_bucket.grant_read_write(callout_function) response_handler_function = _lambda.Function( self, "ResponseHandlerFunction", handler="response_handler.lambda_handler", **global_python_function_arguments) response_handler_function.add_permission( id="ResponseHandlerFunctionLambdaInvokePermission", principal=_iam.ServicePrincipal(service="connect.amazonaws.com"), action="lambda:InvokeFunction", source_account=self.account, source_arn= f"arn:aws:connect:{self.region}:{self.account}:instance/{instance_id}" ) send_task_success_function = _lambda.Function( self, "SendTaskSuccessFunction", handler="send_task_success.lambda_handler", **global_python_function_arguments) send_task_success_function.add_permission( id="SendTaskSuccessFunctionLambdaInvokePermission", principal=_iam.ServicePrincipal(service="connect.amazonaws.com"), action="lambda:InvokeFunction", source_account=self.account, source_arn= f"arn:aws:connect:{self.region}:{self.account}:instance/{instance_id}" ) get_call_result_function = _lambda.Function( self, "GetCallResultFunction", handler="get_call_result.lambda_handler", memory_size=512, **global_python_function_arguments) get_call_result_function.add_environment( key="CallResultDynamoDBTable", value=call_result_table.table_name) get_call_result_function.add_environment(key="S3Bucket", value=file_bucket.bucket_name) call_result_table.grant_read_data(grantee=get_call_result_function) file_bucket.grant_read_write(get_call_result_function) iterator_function = _lambda.Function( self, "IteratorFunction", handler="iterator.lambda_handler", **global_python_function_arguments) iterator_function.add_permission( id="IteratorFunctionLambdaInvokePermission", principal=_iam.ServicePrincipal(service="connect.amazonaws.com"), action="lambda:InvokeFunction", source_account=self.account, source_arn= f"arn:aws:connect:{self.region}:{self.account}:instance/{instance_id}" ) create_appsync_call_task_function = _lambda.Function( self, "CreateAppSyncCallTaskFunction", handler="create_appsync_call_task.lambda_handler", **global_nodeje_function_arguments) create_appsync_call_task_function.add_environment( key="CallSqsQueueUrl", value=call_sqs_queue.queue_url) create_appsync_call_task_function.add_environment( key="CallRecordTableName", value=callout_record_table.table_name) call_sqs_queue.grant_send_messages(create_appsync_call_task_function) callout_record_table.grant_write_data( create_appsync_call_task_function) create_call_report_record_function = _lambda.Function( self, "CreateCallReportRecordFunction", handler="create_call_report_record.lambda_handler", **global_nodeje_function_arguments) create_excel_call_task_function = _lambda.Function( self, "CreateExcelCallTaskFunction", handler="create_excel_call_task.lambda_handler", **global_python_function_arguments) create_excel_call_task_function.add_environment( key="CallSqsQueueUrl", value=call_sqs_queue.queue_url) call_sqs_queue.grant_send_messages(create_excel_call_task_function) create_excel_call_task_function.add_event_source( source=_les.S3EventSource(bucket=file_bucket, events=[_s3.EventType.OBJECT_CREATED], filters=[ _s3.NotificationKeyFilter( prefix="call_task", suffix=".xlsx") ])) start_callout_flow_function = _lambda.Function( self, "StartCalloutFlowFunction", handler="start_call_out_flow.lambda_handler", reserved_concurrent_executions=1, **global_python_function_arguments) start_callout_flow_function.add_environment( key="CallSqsQueueUrl", value=call_sqs_queue.queue_url) start_callout_flow_function.add_environment( key="ResponseHandlerFunctionArn", value=response_handler_function.function_arn) start_callout_flow_function.add_environment( key="IteratorFunctionArn", value=iterator_function.function_arn) start_callout_flow_function.add_environment( key="SendTaskSuccessFunctionArn", value=send_task_success_function.function_arn) start_callout_flow_function.add_environment( key="S3Bucket", value=file_bucket.bucket_name) start_callout_flow_function.add_event_source( source=_les.SqsEventSource(queue=call_sqs_queue, batch_size=1)) file_bucket.grant_read_write(start_callout_flow_function) call_state_machine_definition = { "Comment": "Reading messages from an SQS queue and iteratively processing each message.", "StartAt": "Start", "States": { "Start": { "Type": "Pass", "Next": "Process Call Messages" }, "Process Call Messages": { "Type": "Map", "Next": "Get Call Result", "InputPath": "$", "ItemsPath": "$", "OutputPath": "$.[0]", "Iterator": { "StartAt": "Get Call out job", "States": { "Get Call out job": { "Type": "Task", "Resource": get_callout_job_function.function_arn, "Next": "Callout with AWS Connect" }, "Callout with AWS Connect": { "Type": "Task", "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", "TimeoutSeconds": timeout, "Parameters": { "QueueUrl": async_callout_queue.queue_url, "MessageGroupId": "1", "MessageBody": { "Message.$": "$", "TaskToken.$": "$$.Task.Token" } }, "Catch": [{ "ErrorEquals": ["States.Timeout"], "ResultPath": None, "Next": "Call Timeout" }], "Next": "Save call result" }, "Call Timeout": { "Type": "Pass", "ResultPath": None, "Next": "Save call result" }, "Save call result": { "Type": "Task", "Resource": "arn:aws:states:::dynamodb:putItem", "Parameters": { "TableName": call_result_table.table_name, "Item": { "receiver_id": { "S.$": "$.receiver_id" }, "task_id": { "S.$": "$.task_id" }, "username": { "S.$": "$.username" }, "phone_number": { "S.$": "$.phone_number" }, "status": { "S.$": "$.status" }, "answers": { "S.$": "$.answers" }, "error": { "S.$": "$.error" }, "call_at": { "S.$": "$.call_at" } } }, "ResultPath": "$.Result", "OutputPath": "$.task_id", "End": True } } } }, "Get Call Result": { "Type": "Task", "Resource": get_call_result_function.function_arn, "Next": "Create Call Report Record" }, "Create Call Report Record": { "Type": "Task", "Resource": create_call_report_record_function.function_arn, "Next": "Send Completion message to SNS" }, "Send Completion message to SNS": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "TopicArn": call_job_complete_sns_topic.topic_arn, "Message.$": "$" }, "Next": "Finish" }, "Finish": { "Type": "Succeed" } } } callout_state_machine_role = _iam.Role( self, "CalloutStatesExecutionRole", assumed_by=_iam.ServicePrincipal( f"states.{self.region}.amazonaws.com")) callout_state_machine_role.add_to_policy( _iam.PolicyStatement( actions=[ "sqs:SendMessage", "dynamodb:PutItem", "lambda:InvokeFunction", "SNS:Publish" ], resources=[ async_callout_queue.queue_arn, call_result_table.table_arn, get_callout_job_function.function_arn, get_call_result_function.function_arn, call_job_complete_sns_topic.topic_arn, create_appsync_call_task_function.function_arn, create_call_report_record_function.function_arn ])) callout_state_machine = _sfn.CfnStateMachine( self, "CalloutStateMachine", role_arn=callout_state_machine_role.role_arn, definition_string=json.dumps(call_state_machine_definition)) send_task_success_function.add_to_role_policy( _iam.PolicyStatement(actions=["states:SendTaskSuccess"], resources=[callout_state_machine.ref])) start_callout_flow_function.add_environment( key="CalloutStateMachineArn", value=callout_state_machine.ref) start_callout_flow_function.add_to_role_policy( _iam.PolicyStatement(effect=_iam.Effect.ALLOW, resources=[callout_state_machine.ref], actions=['states:StartExecution'])) user_pool = _cognito.UserPool( self, "UserPool", sign_in_type=_cognito.SignInType.USERNAME) user_pool_client = _cognito.UserPoolClient(self, "UserPoolClient", user_pool=user_pool) appsync_api = _appsync.GraphQLApi( self, "AppSyncApi", name="AWSCalloutApi", user_pool_config=_appsync.UserPoolConfig( user_pool=user_pool, default_action=_appsync.UserPoolDefaultAction.ALLOW), log_config=_appsync.LogConfig( field_log_level=_appsync.FieldLogLevel.ALL), schema_definition_file= f"{pathlib.Path(__file__).parent.absolute()}/schema.graphql") callout_record_ddb_ds = appsync_api.add_dynamo_db_data_source( name="CalloutRecordDdb", description="Callout Record DynamoDB Data Source", table=callout_record_table) callout_record_ddb_ds.create_resolver( type_name="Query", field_name="getLatestCallTaskRecords", request_mapping_template=_appsync.MappingTemplate.from_string( '{"version":"2017-02-28","operation":"Query","index":"CallTypeCreatedAtGlobalIndex","query":{"expression":"call_type = :call_type","expressionValues":{":call_type":{"S":"TASK"}}},"scanIndexForward":false,"limit":${ctx.args.limit}}' ), response_mapping_template=_appsync.MappingTemplate. dynamo_db_result_list()) callout_record_ddb_ds.create_resolver( type_name="Query", field_name="getLatestCallReportRecords", request_mapping_template=_appsync.MappingTemplate.from_string( '{"version":"2017-02-28","operation":"Query","index":"CallTypeCreatedAtGlobalIndex","query":{"expression":"call_type = :call_type","expressionValues":{":call_type":{"S":"REPORT"}}},"scanIndexForward":false,"limit":${ctx.args.limit}}' ), response_mapping_template=_appsync.MappingTemplate. dynamo_db_result_list()) callout_record_ddb_ds.create_resolver( type_name="Mutation", field_name="createCallReport", request_mapping_template=_appsync.MappingTemplate.from_string( '{"version":"2017-02-28","operation":"PutItem","key":{"task_id":{"S":"${ctx.args.report.task_id}"},"created_at":{"N":"${ctx.args.report.created_at}"}},"attributeValues":$util.dynamodb.toMapValuesJson($ctx.args.report)}' ), response_mapping_template=_appsync.MappingTemplate. dynamo_db_result_item()) call_task_lambda_ds = appsync_api.add_lambda_data_source( name="CallTaskLambda", description="Call Task Lambda Data Source", lambda_function=create_appsync_call_task_function) call_task_lambda_ds.create_resolver( type_name="Mutation", field_name="createCallTask", request_mapping_template=_appsync.MappingTemplate.lambda_request( "$utils.toJson($ctx.args)"), response_mapping_template=_appsync.MappingTemplate.lambda_result()) create_call_report_record_function.add_environment( value=appsync_api.graph_ql_url, key="AppSyncGraphQlApiUrl") create_call_report_record_function.add_to_role_policy( statement=_iam.PolicyStatement( effect=_iam.Effect.ALLOW, actions=['appsync:GraphQL'], resources=[ f"{appsync_api.arn}/types/Mutation/fields/createCallReport" ])) core.CfnOutput(self, id="OutputCallSqsQueue", value=call_sqs_queue.queue_arn) core.CfnOutput(self, id="OutputCallJobCompletionSNSTopic", value=call_job_complete_sns_topic.topic_arn) core.CfnOutput(self, id="OutputExcelFileS3Bucket", value=file_bucket.bucket_name) core.CfnOutput(self, id="OutputStaticWebS3Bucket", value=web_bucket.bucket_name) core.CfnOutput(self, id="OutputStaticWebUrl", value=web_bucket.bucket_website_url) identity_pool = _cognito.CfnIdentityPool( self, "IdentityPool", allow_unauthenticated_identities=True, cognito_identity_providers=[ _cognito.CfnIdentityPool.CognitoIdentityProviderProperty( provider_name=user_pool.user_pool_provider_name, client_id=user_pool_client.user_pool_client_id) ]) identity_pool_unauthorized_role = _iam.Role( self, 'IdentityPoolUnAuthorizedRole', assumed_by=_iam.FederatedPrincipal( federated="cognito-identity.amazonaws.com", assume_role_action="sts:AssumeRoleWithWebIdentity", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" } })) identity_pool_unauthorized_role.add_to_policy( _iam.PolicyStatement( effect=_iam.Effect.ALLOW, actions=["appsync:GraphQL"], resources=[ f"{appsync_api.arn}/types/*", # f"{appsync_api.arn}/types/Query/fields/getLatestCallTaskRecords", # f"{appsync_api.arn}/types/Query/fields/getLatestCallReportRecords", # f"{appsync_api.arn}/types/Mutation/fields/createCallRecord", # f"{appsync_api.arn}/types/Subscription/fields/createCallTask", # f"{appsync_api.arn}/types/Subscription/fields/createCallReport" ])) _cognito.CfnIdentityPoolRoleAttachment( self, "CognitoIdentityPoolRoleAttachment", identity_pool_id=identity_pool.ref, roles={ "unauthenticated": identity_pool_unauthorized_role.role_arn }) core.CfnOutput(self, id="UserPoolId", value=user_pool.user_pool_id) core.CfnOutput(self, id="UserPoolClientId", value=user_pool_client.user_pool_client_id) core.CfnOutput(self, id="IdentityPoolId", value=identity_pool.ref)
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) ### S3 ### source_csv_bucket = _s3.Bucket(self, "BYODValidationSourceBucket", versioned=True) target_csv_bucket = _s3.Bucket( self, "BYODValidationTargetBucket", removal_policy=core.RemovalPolicy.RETAIN) webtool_bucket = _s3.Bucket( self, "WebToolBucket", website_index_document="index.html", website_error_document="index.html", public_read_access=True, ) core.CfnOutput(self, "DVTRegion", value=self.region) core.CfnOutput(self, "SourceS3Bucket", value=source_csv_bucket.bucket_name) core.CfnOutput(self, "TargetS3Bucket", value=target_csv_bucket.bucket_name) core.CfnOutput(self, "WebToolS3Bucket", value=webtool_bucket.bucket_name) core.CfnOutput(self, "WebToolUrl", value=webtool_bucket.bucket_website_url) ### Stager Function ### stager_function = _lambda.Function(self, "StagerFunction", runtime=_lambda.Runtime.NODEJS_12_X, code=_lambda.Code.from_asset( os.path.join( dirname, "lambda", "stager")), handler='index.handler') stager_function.add_environment("REGION", self.region) stager_function.add_environment("SOURCE_BUCKET", source_csv_bucket.bucket_name) stager_function.add_environment("STAGE_BUCKET", target_csv_bucket.bucket_name) source_csv_bucket.grant_read(stager_function) target_csv_bucket.grant_put(stager_function) core.CfnOutput(self, "StagerLambdaFunction", value=stager_function.function_name) ### Profiling Queue profiling_job_queue = _sqs.Queue(self, "ProfilingJobQueue") core.CfnOutput(self, "SQSProfileQueue", value=profiling_job_queue.queue_url) ### Cognito ### userpool = _cognito.UserPool(self, "WebToolUserPool", user_pool_name="byod-webtool-userpool", self_sign_up_enabled=True, auto_verify={ "email": True, "phone": False }, user_verification={ "email_subject": "Your verification code", "email_body": "Your verification code is {####}", "email_style": _cognito.VerificationEmailStyle.CODE }, standard_attributes={ "email": { "required": True, "mutable": False } }, password_policy={}) client = userpool.add_client( "webtool-app-client", auth_flows={ "custom": True, "user_password": True, "user_srp": True, #"refresh_token": True }) identity_pool = _cognito.CfnIdentityPool( self, "WebToolCognitoIdentityPool", allow_unauthenticated_identities=True) identity_pool.add_property_override( "CognitoIdentityProviders", [{ "ClientId": client.user_pool_client_id, "ProviderName": userpool.user_pool_provider_name }]) auth_role = _iam.Role( self, "CognitoAuthRole", assumed_by=WebIdentityPrincipal( "cognito-identity.amazonaws.com", { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" } })) auth_role.add_to_policy( PolicyStatement(effect=Effect.ALLOW, actions=["s3:GetObject", "s3:PutObject"], resources=["%s/*" % target_csv_bucket.bucket_arn])) auth_role.add_to_policy( PolicyStatement(effect=Effect.ALLOW, actions=["lambda:invokeFunction"], resources=[stager_function.function_arn])) auth_role.add_to_policy( PolicyStatement(effect=Effect.ALLOW, actions=["sqs:*"], resources=[profiling_job_queue.queue_arn])) unauth_role = _iam.Role( self, "CognitoUnauthRole", assumed_by=_iam.WebIdentityPrincipal( "cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" } })) identity_pool_policy = _cognito.CfnIdentityPoolRoleAttachment( self, "WebToolCognitoIdentityPoolPolicy", identity_pool_id=identity_pool.ref, roles={ 'unauthenticated': unauth_role.role_arn, 'authenticated': auth_role.role_arn }) core.CfnOutput(self, "UserPoolId", value=userpool.user_pool_id) core.CfnOutput(self, "IdentityPoolId", value=identity_pool.ref) core.CfnOutput(self, "ClientId", value=client.user_pool_client_id) core.CfnOutput(self, "ProviderName", value=userpool.user_pool_provider_name) ### DynamoDB ### validation_job_table = _dynamodb.Table( self, "ValidationJobTable", partition_key=_dynamodb.Attribute( name="id", type=_dynamodb.AttributeType.STRING)) ## AppSync ### api = _appsync.GraphqlApi( self, "Api", name="validation-job-api", schema=_appsync.Schema.from_asset( os.path.join(dirname, "api", "schema.graphql")), authorization_config=AuthorizationConfig( default_authorization=AuthorizationMode( authorization_type=AuthorizationType.USER_POOL, user_pool_config=UserPoolConfig(user_pool=userpool))), log_config=LogConfig(exclude_verbose_content=False, field_log_level=FieldLogLevel.ALL)) api_ds = api.add_dynamo_db_data_source("ValidationJobDataSource", validation_job_table) from aws_cdk.aws_appsync import MappingTemplate api_ds.create_resolver( type_name="Query", field_name="listJobss", request_mapping_template=MappingTemplate.from_file( os.path.join(dirname, "api", "resolvers", "Query.listJobss.req.vtl")), response_mapping_template=MappingTemplate.from_file( os.path.join(dirname, "api", "resolvers", "Query.listJobss.res.vtl"))) api_ds.create_resolver( type_name="Query", field_name="getJobs", request_mapping_template=MappingTemplate.from_file( os.path.join(dirname, "api", "resolvers", "Query.getJobs.req.vtl")), response_mapping_template=MappingTemplate.from_file( os.path.join(dirname, "api", "resolvers", "Query.getJobs.res.vtl"))) core.CfnOutput(self, "GraphQLEndpoint", value=api.graphql_url) ### SQS ### validation_job_queue = _sqs.Queue(self, "ValidationJobQueue") ### Lambda ### validation_trigger_function = _lambda.Function( self, "ValidationTriggerFunction", runtime=_lambda.Runtime.PYTHON_3_8, code=_lambda.Code.from_asset( os.path.join(dirname, "lambda", "validation_trigger")), handler='lambda_function.lambda_handler') validation_trigger_function.add_environment( "TABLE_NAME", validation_job_table.table_name) validation_trigger_function.add_environment( "QUEUE_URL", validation_job_queue.queue_url) validation_trigger_function.add_event_source( _S3EventSource(source_csv_bucket, events=[_s3.EventType.OBJECT_CREATED])) source_csv_bucket.grant_read(validation_trigger_function) validation_job_table.grant_read_write_data(validation_trigger_function) validation_job_queue.grant_send_messages(validation_trigger_function) ### ECS Fargate ### validation_fargate_asset = _ecr_assets.DockerImageAsset( self, "ValidationBuildImage", directory=os.path.join(dirname, "fargate", "validation")) profiling_fargate_asset = _ecr_assets.DockerImageAsset( self, "ProfilingBuildImage", directory=os.path.join(dirname, "fargate", "profiling")) vpc = _ec2.Vpc(self, "VPC", max_azs=3) cluster = _ecs.Cluster(self, "ECSCluster", vpc=vpc) validation_fargate_service = _ecs_patterns.QueueProcessingFargateService( self, "ValidationFargateService", cluster=cluster, cpu=4096, memory_limit_mib=30720, enable_logging=True, image=_ecs.ContainerImage.from_docker_image_asset( validation_fargate_asset), environment={ "TABLE_NAME": validation_job_table.table_name, "QUEUE_URL": validation_job_queue.queue_url, "SOURCE_BUCKET_NAME": source_csv_bucket.bucket_name, "TARGET_BUCKET_NAME": target_csv_bucket.bucket_name, "REGION": self.region }, queue=validation_job_queue, max_scaling_capacity=2, max_healthy_percent=200, min_healthy_percent=66) validation_fargate_service.task_definition.task_role.add_managed_policy( _iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonDynamoDBFullAccess")) validation_fargate_service.task_definition.task_role.add_managed_policy( _iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonS3FullAccess")) profiling_fargate_service = _ecs_patterns.QueueProcessingFargateService( self, "ProfilingFargateService", cluster=cluster, cpu=4096, memory_limit_mib=30720, enable_logging=True, image=_ecs.ContainerImage.from_docker_image_asset( profiling_fargate_asset), environment={ "TABLE_NAME": validation_job_table.table_name, "QUEUE_URL": profiling_job_queue.queue_url, "SOURCE_BUCKET_NAME": source_csv_bucket.bucket_name, "TARGET_BUCKET_NAME": target_csv_bucket.bucket_name, "REGION": self.region }, queue=profiling_job_queue, max_scaling_capacity=2, max_healthy_percent=200, min_healthy_percent=66) profiling_fargate_service.task_definition.task_role.add_managed_policy( _iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonDynamoDBFullAccess")) profiling_fargate_service.task_definition.task_role.add_managed_policy( _iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonS3FullAccess"))
def __init__(self, scope: core.Construct, id: str, domain_prefix: str, other_account: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) user_pool = cognito.CfnUserPool( scope=self, id="user-pool", admin_create_user_config=cognito.CfnUserPool.AdminCreateUserConfigProperty( allow_admin_create_user_only=True, ), policies=cognito.CfnUserPool.PoliciesProperty( password_policy=cognito.CfnUserPool.PasswordPolicyProperty( minimum_length=20, )), username_attributes=["email"], auto_verified_attributes=["email"], ) cognito.CfnUserPoolDomain( scope=self, id="cognito-user-pool-domain", domain=f"{domain_prefix}-{core.Aws.ACCOUNT_ID}", user_pool_id=user_pool.ref, ) id_pool = cognito.CfnIdentityPool( scope=self, id="identity-pool", allow_unauthenticated_identities=False, cognito_identity_providers=[], ) auth_role = iam.Role( scope=self, id="auth-role", assumed_by=iam.FederatedPrincipal( federated="cognito-identity.amazonaws.com", conditions={ "StringEquals": {"cognito-identity.amazonaws.com:aud": id_pool.ref}, "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "authenticated"}, }, assume_role_action="sts:AssumeRoleWithWebIdentity"), ) es_role = iam.Role( scope=self, id="es-role", assumed_by=iam.ServicePrincipal('es.amazonaws.com'), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( managed_policy_name="AmazonESCognitoAccess" ) ], ) es_domain = elasticsearch.CfnDomain( scope=self, id="search-domain", elasticsearch_cluster_config=elasticsearch.CfnDomain.ElasticsearchClusterConfigProperty( instance_count=2, instance_type="m5.large.elasticsearch", ), node_to_node_encryption_options=elasticsearch.CfnDomain.NodeToNodeEncryptionOptionsProperty( enabled=True), encryption_at_rest_options=elasticsearch.CfnDomain.EncryptionAtRestOptionsProperty( enabled=True), ebs_options=elasticsearch.CfnDomain.EBSOptionsProperty( ebs_enabled=True, volume_size=20), elasticsearch_version="7.4", domain_name=domain_prefix, access_policies={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": auth_role.role_arn }, "Action": [ "es:ESHttpGet", "es:ESHttpPut", "es:ESHttpPost", "es:ESHttpDelete" ], "Resource": "arn:aws:es:" + core.Aws.REGION + ":" + core.Aws.ACCOUNT_ID + ":domain/" + domain_prefix + "/*" }, ] }, ) es_domain.add_property_override( 'CognitoOptions.Enabled', True) es_domain.add_property_override( 'CognitoOptions.IdentityPoolId', id_pool.ref) es_domain.add_property_override( 'CognitoOptions.RoleArn', es_role.role_arn) es_domain.add_property_override( 'CognitoOptions.UserPoolId', user_pool.ref) cognito.CfnIdentityPoolRoleAttachment( scope=self, id='user-pool-role-attachment', identity_pool_id=id_pool.ref, roles={ 'authenticated': auth_role.role_arn } ) es_external_role = iam.Role( scope=self, id="logger-role", assumed_by=iam.CompositePrincipal( iam.ServicePrincipal("lambda.amazonaws.com"), iam.AccountPrincipal(other_account), ), description="role to use elastic search assumed by lambda", inline_policies={ "es_policy": iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "es:ESHttpPost", ], resources=[ es_domain.attr_arn + "/*", ], )]), }, ) core.CfnOutput( scope=self, id="es-host", value=es_domain.attr_domain_endpoint, ) core.CfnOutput( scope=self, id="es-region", value=core.Aws.REGION, ) core.CfnOutput( scope=self, id="es-external-role", value=es_external_role.role_arn, )
def __init__(self, scope: core.Construct, id: str, common_stack: CommonStack, **kwargs) -> None: super().__init__(scope, id, **kwargs) self._supported_in_region = self.are_services_supported_in_region( ["cognito-identity", "kinesis"]) identity_pool = cognito.CfnIdentityPool( self, "pinpoint_integ_test_android", allow_unauthenticated_identities=True) unauthenticated_role = iam.Role( self, "CognitoDefaultUnauthenticatedRole", assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" }, }, "sts:AssumeRoleWithWebIdentity", ), ) unauthenticated_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=["cognito-sync:*", "kinesis:*", "firehose:*"], resources=["*"], )) cognito.CfnIdentityPoolRoleAttachment( self, "DefaultValid", identity_pool_id=identity_pool.ref, roles={"unauthenticated": unauthenticated_role.role_arn}, ) firehose_s3_role = iam.Role( self, "FirehoseS3Role", assumed_by=iam.ServicePrincipal("firehose.amazonaws.com")) firehose_s3_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:PutObject", ], resources=["*"], )) ingest_bucket = s3.Bucket(self, "test-aws-android-sdk-firehose-bucket") s3_dest_config = firehose.CfnDeliveryStream.S3DestinationConfigurationProperty( bucket_arn=ingest_bucket.bucket_arn, buffering_hints=firehose.CfnDeliveryStream.BufferingHintsProperty( interval_in_seconds=60, size_in_m_bs=5), compression_format="UNCOMPRESSED", role_arn=firehose_s3_role.role_arn, ) firehose_test = firehose.CfnDeliveryStream( self, "kinesis_firehose_recorder_test", s3_destination_configuration=s3_dest_config) self._parameters_to_save = { "firehose_name": firehose_test.ref, "identity_pool_id": identity_pool.ref, } self.save_parameters_in_parameter_store(platform=Platform.ANDROID) common_stack.add_to_common_role_policies(self)
def __init__(self, scope: core.Construct, id: str, common_stack: CommonStack, **kwargs) -> None: super().__init__(scope, id, **kwargs) self._supported_in_region = self.is_service_supported_in_region() app = pinpoint.CfnApp(self, "android-integ-test", name="android-integ-test") identity_pool = cognito.CfnIdentityPool( self, "pinpoint_integ_test_android", allow_unauthenticated_identities=True) unauthenticated_role = iam.Role( self, "CognitoDefaultUnauthenticatedRole", assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", { "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" }, }, "sts:AssumeRoleWithWebIdentity", ), ) unauthenticated_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=["cognito-sync:*", "mobiletargeting:PutEvents"], resources=["*"], )) cognito.CfnIdentityPoolRoleAttachment( self, "DefaultValid", identity_pool_id=identity_pool.ref, roles={"unauthenticated": unauthenticated_role.role_arn}, ) self._parameters_to_save = { "identity_pool_id": identity_pool.ref, "AppId": app.ref, "Region": core.Aws.REGION, } self.save_parameters_in_parameter_store(platform=Platform.ANDROID) stack_policy = iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "mobileanalytics:PutEvents", "mobiletargeting:PutEvents", "mobiletargeting:UpdateEndpoint", ], resources=["*"], ) common_stack.add_to_common_role_policies(self, policy_to_add=stack_policy)
def __init__(self, scope: core.Construct, construct_id: str, cognito_prefix: str, stack_log_level: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # The code that defines your stack goes here # Cognito User Pool for Kibana Access self.es_user_pool = _cognito.UserPool( self, "esCognitoUserPool", user_pool_name=f"{cognito_prefix}-log-parser-es-users", self_sign_up_enabled=False, sign_in_aliases=_cognito.SignInAliases(username=True, email=True), sign_in_case_sensitive=False, standard_attributes={ "email": { "required": True, "mutable": False }, "fullname": { "required": False, "mutable": True } }, password_policy=_cognito.PasswordPolicy(min_length=8), auto_verify=_cognito.AutoVerifiedAttrs(email=True)) # Create User Pool Domain to enable SignUp, SignIn, Auth & Callback Urls es_auth_domain = _cognito.UserPoolDomain( self, "esCognitoUserPoolDomain", user_pool=self.es_user_pool, cognito_domain=_cognito.CognitoDomainOptions( domain_prefix=f"{cognito_prefix}-{core.Aws.ACCOUNT_ID}")) # Role Authenticated Cognito Users will assume in ES Service self.es_role = _iam.Role( self, "esKibanaIamRole", assumed_by=_iam.ServicePrincipal("es.amazonaws.com"), managed_policies=[ _iam.ManagedPolicy.from_aws_managed_policy_name( managed_policy_name="AmazonESCognitoAccess") ], ) # Create Cognito Federated Identity Pool to exchange Cognito auth token for AWS Token self.es_id_pool = _cognito.CfnIdentityPool( self, "esIdentityPool", allow_unauthenticated_identities=False, cognito_identity_providers=[], ) # Role For Cognito Federated Idenity Pool Authenticated Users self.es_auth_role = _iam.Role( self, "esAuthIamRole", assumed_by=_iam.FederatedPrincipal( federated="cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": self.es_id_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity"), ) # Attach a Role to Cognito Federated Idenity Pool Authenticated Users _cognito.CfnIdentityPoolRoleAttachment( self, "cognitoFederatedIdentityPoolRoleAttachment", identity_pool_id=self.es_id_pool.ref, roles={"authenticated": self.es_auth_role.role_arn}) ########################################### ################# OUTPUTS ################# ########################################### output_0 = core.CfnOutput( self, "AutomationFrom", value=f"{GlobalArgs.SOURCE_INFO}", description= "To know more about this automation stack, check out our github page." ) output_1 = core.CfnOutput( self, "CreateCognitoUserConsole", value= f"https://{core.Aws.REGION}.console.aws.amazon.com/cognito/users?region={core.Aws.REGION}#/pool/{self.es_user_pool.user_pool_id}/users", description="Create a new user in the user pool here.")