AWSTemplateFormatVersion: 2010-09-09 Transform: 'AWS::Serverless-2016-10-31' Description: This script implement a vending machine solution, a way to remove the complexity to provisioning infrastructur for teams. Parameters: EmailApprover: Type: String Description: The email address of the user or team that will approve a product request. Default: '' EmailCognito: Type: String Description: The email address of the user that will be created in the Cognito User Pool. Leave empty to skip user creation. Default: '' EmailNotification: Type: String Description: The email address of the user or team that will receive notifications. Default: '' OrganizationId: Type: String Description: The Organization ID who will access the S3 Bucket with CFN Templates. Default: '' KmsMaterKeyId: Type: String Description: The KMS Master Key ID to encript the SNS content. Default: '' Resources: #Front-end CloudfrontAuthorizationEdge: Type: AWS::Serverless::Application Properties: Location: ApplicationId: arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge SemanticVersion: 2.0.13 Parameters: EmailAddress: !Ref EmailCognito RedirectPathSignIn: '/parseauth' RedirectPathSignOut: '/' RedirectPathAuthRefresh: '/refreshauth' SignOutUrl: '/signout' AlternateDomainNames: '' CookieSettings: |- { "idToken": null, "accessToken": null, "refreshToken": null, "nonce": null } OAuthScopes: 'phone, email, profile, openid, aws.cognito.signin.user.admin' HttpHeaders: |- { "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com; object-src 'none'; connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com", "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", "Referrer-Policy": "same-origin", "X-XSS-Protection": "1; mode=block", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff" } EnableSPAMode: 'true' CreateCloudFrontDistribution: 'true' CookieCompatibility: amplify AdditionalCookies: '{}' UserPoolArn: '' UserPoolClientId: '' UserPoolGroupName: '' Version: 2.0.13 LogLevel: none PermissionsBoundaryPolicyArn: '' RewritePathWithTrailingSlashToIndex: 'false' TimeoutInMinutes: 60 ##upload website #DynamoDB deployDynamoDBTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: stackinstanceData AttributeType: S # - AttributeName: stacksetData # AttributeType: S - AttributeName: id AttributeType: S KeySchema: - AttributeName: stackinstanceData KeyType: HASH - AttributeName: id KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: deploy productsDynamoDBTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: stacksetData AttributeType: S KeySchema: - AttributeName: stacksetData KeyType: HASH - AttributeName: id KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: products #StepFuctions SFProvisioningProduct: DependsOn: - SNSInfraNotificationTopic - StatesExecutionRole - GetDeployDataDynamoSF - CreateStackSetSF - CreateStackInstanceSF - ExecutionApi Type: "AWS::StepFunctions::StateMachine" Properties: DefinitionString: !Sub | { "Comment": "Vending Machine - Deploy a product in a destination account", "StartAt": "Lambda Callback", "States": { "Lambda Callback": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken", "Parameters": { "FunctionName": "${LambdaHumanApprovalSendEmailFunction.Arn}", "Payload": { "Input.$": "$", "ExecutionContext.$": "$$", "APIGatewayEndpoint": "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" } }, "Next": "ManualApprovalChoiceState" }, "ManualApprovalChoiceState": { "Type": "Choice", "Choices": [ { "Variable": "$.Status", "StringEquals": "Approved! Task approved", "Next": "dynamo" }, { "Variable": "$.Status", "StringEquals": "Rejected! Task rejected", "Next": "notify-reject" } ], "Default": "notify-reject" }, "dynamo": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "${GetDeployDataDynamoSF.Arn}", "Payload": { "Input.$": "$" } }, "Next": "CreateStackSet" }, "CreateStackSet": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "${CreateStackSetSF.Arn}", "Payload": { "Input.$": "$" } }, "Next": "wait" }, "wait": { "Type": "Wait", "Seconds": 30, "Next": "CreateStackSetInstance" }, "CreateStackSetInstance": { "Type": "Task", "InputPath": "$.Payload", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "${CreateStackInstanceSF.Arn}", "Payload": { "Input.$": "$" } }, "Next": "notify-accept" }, "notify-reject": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "TopicArn": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:SNSInfra", "Message": "Production creation rejected" }, "End": true }, "notify-accept": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "TopicArn": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:SNSInfra", "Message": "Product creation started" }, "End": true } } } RoleArn: !GetAtt 'StatesExecutionRole.Arn' StatesExecutionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: !Sub 'states.${AWS::Region}.amazonaws.com' Action: 'sts:AssumeRole' Policies: - PolicyName: lambda PolicyDocument: Statement: - Effect: Allow Action: 'lambda:InvokeFunction' Resource: '*' # - !GetAtt 'FunctionSendMessage.Arn' # - !GetAtt 'FunctionFetchActivityCount.Arn' SNSInfraNotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: SNSInfra KmsMasterKeyId: !Ref KmsMaterKeyId Subscription: - Endpoint: !Ref EmailNotification Protocol: email #lambdas GetDeployDataDynamoSF: Type: AWS::Lambda::Function Properties: Handler: index.handler Timeout: 15 Role: !GetAtt - LambdaExecutionRole - Arn Runtime: nodejs12.x Code: ZipFile: | const AWS = require('aws-sdk'); const dynamo = new AWS.DynamoDB.DocumentClient(); exports.handler = function(event, context, callback) { console.log('Received event:', JSON.stringify(event)); var data = event.Input; var id = data.id if (data.productId) { id = data.productId; } const params = { TableName: "deploy", Key: { "id": id } }; console.log('DATA:', JSON.stringify(data)); dynamo.get(params, callback); }; CreateStackSetSF: Type: AWS::Lambda::Function Properties: Handler: index.handler Timeout: 15 Role: !GetAtt - LambdaExecutionRole - Arn Runtime: nodejs12.x Code: ZipFile: | const AWS = require('aws-sdk'); const CF = new AWS.CloudFormation(); exports.handler = async (event, context, callback) => { console.log('EVENT: ', JSON.stringify(event)); var stackinstanceData = event.Input.Payload.Item.stackinstanceData; var stacksetData = event.Input.Payload.Item.stacksetData; var res = event.Input; const params = { StackSetName: stacksetData.stack.stacksetName, TemplateURL: stacksetData.stack.templateUrl, Capabilities: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND' ], Parameters: stacksetData.stack.parameters, Tags: stackinstanceData.tags, }; CF.createStackSet(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); callback(null, res); }; CreateStackInstanceSF: Type: AWS::Lambda::Function Properties: Handler: index.handler Timeout: 15 Role: !GetAtt - LambdaExecutionRole - Arn Runtime: nodejs12.x Code: ZipFile: | const AWS = require('aws-sdk'); const CF = new AWS.CloudFormation(); const OperationId = ''; console.log('Loading function'); exports.handler = async (event, context, callback) => { console.log('EVENT: ', JSON.stringify(event)); var stackinstanceData = event.Input.Payload.Item.stackinstanceData; var stacksetData = event.Input.Payload.Item.stacksetData; const params = { Regions: [ stackinstanceData.stregions ], StackSetName: stacksetData.stack.stacksetName, Accounts: [ stackinstanceData.account ], ParameterOverrides: stackinstanceData.parameterOverrides, OperationPreferences: { FailureTolerancePercentage: '50', MaxConcurrentPercentage: '100' } }; var stackinstance = CF.createStackInstances(params, function(err, data) { if (err) { console.log(err, err.stack); } // an error occurred else { console.log(data); // successful response } }); event.Input.operationId = stackinstance.OperationId callback(null, event); }; LambdaExecutionRole: Type: 'AWS::IAM::Role' Properties: Policies: - PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - 'arn:aws:logs:*:*:*' Effect: Allow - Action: - 'dynamodb:DeleteItem' - 'dynamodb:GetItem' - 'dynamodb:PutItem' - 'dynamodb:Scan' - 'dynamodb:UpdateItem' Resource: - !GetAtt - deployDynamoDBTable - Arn - !GetAtt - productsDynamoDBTable - Arn Effect: Allow - Action: - 'cloudformation:CreateChangeSet' - 'cloudformation:CreateStack' - 'cloudformation:CreateStackInstances' - 'cloudformation:CreateStackSet' - 'cloudformation:CreateUploadBucket' - 'cloudformation:DeleteStack' - 'cloudformation:DeleteStackInstances' - 'cloudformation:DeleteStackSet' - 'cloudformation:DescribeChangeSet' - 'cloudformation:DescribeStackEvents' - 'cloudformation:DescribeStackInstance' - 'cloudformation:DescribeStackResource' - 'cloudformation:DescribeStackResourceDrifts' - 'cloudformation:DescribeStackResources' - 'cloudformation:DescribeStackSet' - 'cloudformation:DescribeStackSetOperation' - 'cloudformation:DescribeStacks' - 'cloudformation:DescribeType' - 'cloudformation:DescribeTypeRegistration' - 'cloudformation:DetectStackDrift' - 'cloudformation:DetectStackResourceDrift' - 'cloudformation:EstimateTemplateCost' - 'cloudformation:ExecuteChangeSet' - 'cloudformation:GetStackPolicy' - 'cloudformation:GetTemplate' - 'cloudformation:GetTemplateSummary' - 'cloudformation:ImportStacksToStackSet' - 'cloudformation:ListStackInstances' - 'cloudformation:ListStackResources' - 'cloudformation:ListStackSetOperationResults' - 'cloudformation:ListStackSetOperations' - 'cloudformation:ListStackSets' - 'cloudformation:ListStacks' - 'cloudformation:StopStackSetOperation' - 'cloudformation:TagResource' - 'cloudformation:UntagResource' - 'cloudformation:UpdateStack' - 'cloudformation:UpdateStackInstances' - 'cloudformation:UpdateStackSet' - 'cloudformation:ValidateTemplate' Resource: '*' Effect: Allow AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: - 'sts:AssumeRole' Effect: Allow Principal: Service: - lambda.amazonaws.com VendingMachine: DependsOn: SFProvisioningProduct Type: AWS::Lambda::Function Properties: Handler: index.handler Timeout: 15 Environment: Variables: SF_PROVISIONING_PRODUCT_ARN: !GetAtt 'SFProvisioningProduct.Arn' Role: !GetAtt - VmLambdaExecutionRole - Arn Runtime: nodejs12.x Code: ZipFile: | const AWS = require('aws-sdk'); const dynamo = new AWS.DynamoDB.DocumentClient(); const stepfunctions = new AWS.StepFunctions(); exports.handler = function(event, context, callback) { console.log('Received event:', JSON.stringify(event.body, null, 2)); var data = JSON.parse(event.body); var operation = data.operation; var resp = { "headers": { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers' : '*' }, "isBase64Encoded": false }; if (event.tableName) { event.payload.TableName = data.tableName; } switch (operation) { case 'create': dynamo.put(data.payload).promise().then(result => { resp.statusCode = 200; resp.body = JSON.stringify(result); callback(null, resp); }).catch(error => { console.error(error); resp.statusCode = 500; resp.body = JSON.stringify(error); callback(null, resp); return; }); break; case 'read': dynamo.get(data.payload).promise().then(result => { resp.statusCode = 200; resp.body = JSON.stringify(result.Item); callback(null, resp); }).catch(error => { console.error(error); resp.statusCode = 500; resp.body = JSON.stringify(error); callback(null, resp); return; }); break; case 'update': dynamo.update(data.payload, callback); break; case 'delete': dynamo.delete(data.payload).promise().then(result => { resp.statusCode = 200; resp.body = JSON.stringify(result); callback(null, resp); }).catch(error => { console.error(error); resp.statusCode = 500; resp.body = JSON.stringify(error); callback(null, resp); return; }); break; case 'list': dynamo.scan(data.payload).promise().then(result => { resp.statusCode = 200; resp.body = JSON.stringify(result); callback(null, resp); }).catch(error => { console.error(error); resp.statusCode = 500; resp.body = JSON.stringify(error); callback(null, resp); return; }); break; case 'deploy': dynamo.put(data.payload, callback); var params = { stateMachineArn: process.env.SF_PROVISIONING_PRODUCT_ARN, input: JSON.stringify(data.payload.Item) }; stepfunctions.startExecution(params).promise().then(result => { resp.statusCode = 200; resp.body = JSON.stringify(result); callback(null, resp); }).catch(error => { console.error(error); resp.statusCode = 500; resp.body = JSON.stringify(error); callback(null, resp); return; }); break; default: callback(`Unknown operation: ${operation}`); } }; VmLambdaExecutionRole: Type: 'AWS::IAM::Role' Properties: Policies: - PolicyName: LambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'states:StartExecution' - 'states:StopExecution' - 'states:StartSyncExecution' Resource: !GetAtt - SFProvisioningProduct - Arn Effect: Allow - Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - 'arn:aws:logs:*:*:*' Effect: Allow - Action: - 'dynamodb:DeleteItem' - 'dynamodb:GetItem' - 'dynamodb:PutItem' - 'dynamodb:Scan' - 'dynamodb:UpdateItem' Resource: - !GetAtt - deployDynamoDBTable - Arn - !GetAtt - productsDynamoDBTable - Arn Effect: Allow AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: - 'sts:AssumeRole' Effect: Allow Principal: Service: - lambda.amazonaws.com LambdaInvokePermission: DependsOn: VendingMachineAPI Type: 'AWS::Lambda::Permission' Properties: Action: 'lambda:InvokeFunction' Principal: apigateway.amazonaws.com FunctionName: !GetAtt - VendingMachine - Arn SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${VendingMachineAPI}/*/* # - { RestApi: Ref: VendingMachine-API } #API Gateway VendingMachineAPI: DependsOn: - CloudfrontAuthorizationEdge - VendingMachine Type: AWS::ApiGateway::RestApi Properties: Name: VendingMachine API Description: The Vending machine API Gateway Body: swagger: "2.0" info: description: "The Vending machine API Gateway" version: "2021-10-14T18:34:54Z" title: "VendingMachineAPI" basePath: "/default" schemes: - "https" paths: /VendingMachine: post: produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - vendingmachine-userpool: [] x-amazon-apigateway-integration: httpMethod: "POST" uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${VendingMachine.Arn}/invocations" responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: "when_no_match" contentHandling: "CONVERT_TO_TEXT" type: "aws_proxy" options: consumes: - "application/json" produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Credentials: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Credentials: "'true'" method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates: application/json: "{\"statusCode\": 200}" passthroughBehavior: "when_no_match" type: "mock" securityDefinitions: vendingmachine-userpool: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "cognito_user_pools" x-amazon-apigateway-authorizer: type: "cognito_user_pools" providerARNs: - !Join - '' - - 'arn:aws:cognito-idp:' - !Ref AWS::Region - ':' - !Ref AWS::AccountId - ':userpool/' - Fn::GetAtt: - CloudfrontAuthorizationEdge - Outputs.UserPoolId definitions: Empty: type: "object" title: "Empty Schema" default: DependsOn: - VendingMachineAPI - deployment Type: AWS::ApiGateway::Stage Properties: StageName: default Description: Default Stage RestApiId: !Ref VendingMachineAPI DeploymentId: !Ref deployment deployment: Type: 'AWS::ApiGateway::Deployment' Properties: RestApiId: !Ref VendingMachineAPI Description: Vending Machine API deploy # Begin API Gateway Resources ExecutionApi: Type: "AWS::ApiGateway::RestApi" Properties: Name: "Human approval endpoint" Description: "HTTP Endpoint backed by API Gateway and Lambda" FailOnWarnings: true ExecutionResource: Type: 'AWS::ApiGateway::Resource' Properties: RestApiId: !Ref ExecutionApi ParentId: !GetAtt "ExecutionApi.RootResourceId" PathPart: execution ExecutionMethod: Type: "AWS::ApiGateway::Method" Properties: AuthorizationType: NONE HttpMethod: GET Integration: Type: AWS IntegrationHttpMethod: POST Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaApprovalFunction.Arn}/invocations" IntegrationResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: "integration.response.body.headers.Location" RequestTemplates: application/json: | { "body" : $input.json('$'), "headers": { #foreach($header in $input.params().header.keySet()) "$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end #end }, "method": "$context.httpMethod", "params": { #foreach($param in $input.params().path.keySet()) "$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end #end }, "query": { #foreach($queryParam in $input.params().querystring.keySet()) "$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end #end } } ResourceId: !Ref ExecutionResource RestApiId: !Ref ExecutionApi MethodResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: true ApiGatewayAccount: Type: 'AWS::ApiGateway::Account' Properties: CloudWatchRoleArn: !GetAtt "ApiGatewayCloudWatchLogsRole.Arn" ApiGatewayCloudWatchLogsRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: ApiGatewayLogsPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" ExecutionApiStage: DependsOn: - ApiGatewayAccount Type: 'AWS::ApiGateway::Stage' Properties: DeploymentId: !Ref ApiDeployment MethodSettings: - DataTraceEnabled: true HttpMethod: '*' LoggingLevel: INFO ResourcePath: /* RestApiId: !Ref ExecutionApi StageName: states ApiDeployment: Type: "AWS::ApiGateway::Deployment" DependsOn: - ExecutionMethod Properties: RestApiId: !Ref ExecutionApi StageName: DummyStage # End API Gateway Resources # Begin # Lambda that will be invoked by API Gateway LambdaApprovalFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: Fn::Sub: | const AWS = require('aws-sdk'); var redirectToStepFunctions = function(lambdaArn, statemachineName, executionName, callback) { const lambdaArnTokens = lambdaArn.split(":"); const partition = lambdaArnTokens[1]; const region = lambdaArnTokens[3]; const accountId = lambdaArnTokens[4]; console.log("partition=" + partition); console.log("region=" + region); console.log("accountId=" + accountId); const executionArn = "arn:" + partition + ":states:" + region + ":" + accountId + ":execution:" + statemachineName + ":" + executionName; console.log("executionArn=" + executionArn); const url = "https://console.aws.amazon.com/states/home?region=" + region + "#/executions/details/" + executionArn; callback(null, { statusCode: 302, headers: { Location: url } }); }; exports.handler = (event, context, callback) => { console.log('Event= ' + JSON.stringify(event)); const action = event.query.action; const taskToken = event.query.taskToken; const statemachineName = event.query.sm; const executionName = event.query.ex; const stepfunctions = new AWS.StepFunctions(); var message = ""; if (action === "approve") { message = { "Status": "Approved! Task approved by ${EmailApprover}" }; } else if (action === "reject") { message = { "Status": "Rejected! Task rejected by ${EmailApprover}" }; } else { console.error("Unrecognized action. Expected: approve, reject."); callback({"Status": "Failed to process the request. Unrecognized Action."}); } stepfunctions.sendTaskSuccess({ output: JSON.stringify(message), taskToken: event.query.taskToken }) .promise() .then(function(data) { redirectToStepFunctions(context.invokedFunctionArn, statemachineName, executionName, callback); }).catch(function(err) { console.error(err, err.stack); callback(err); }); } Description: Lambda function that callback to AWS Step Functions FunctionName: LambdaApprovalFunction Handler: index.handler Role: !GetAtt "LambdaApiGatewayIAMRole.Arn" Runtime: nodejs12.x LambdaApiGatewayInvoke: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt "LambdaApprovalFunction.Arn" Principal: "apigateway.amazonaws.com" SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ExecutionApi}/*" LambdaApiGatewayIAMRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: - "sts:AssumeRole" Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: StepFunctionsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "states:SendTaskFailure" - "states:SendTaskSuccess" Resource: "*" # End Lambda that will be invoked by API Gateway SNSHumanApprovalEmailTopic: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: !Ref KmsMaterKeyId Subscription: - Endpoint: !Sub ${EmailApprover} Protocol: email LambdaHumanApprovalSendEmailFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt LambdaSendEmailExecutionRole.Arn Runtime: "nodejs12.x" Timeout: "25" Code: ZipFile: Fn::Sub: | console.log('Loading function'); const AWS = require('aws-sdk'); exports.lambda_handler = (event, context, callback) => { console.log('event= ' + JSON.stringify(event)); console.log('context= ' + JSON.stringify(context)); const executionContext = event.ExecutionContext; console.log('executionContext= ' + executionContext); const executionName = executionContext.Execution.Name; console.log('executionName= ' + executionName); const statemachineName = executionContext.StateMachine.Name; console.log('statemachineName= ' + statemachineName); const taskToken = executionContext.Task.Token; console.log('taskToken= ' + taskToken); const apigwEndpint = event.APIGatewayEndpoint; console.log('apigwEndpint = ' + apigwEndpint) const approveEndpoint = apigwEndpint + "/execution?action=approve&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('approveEndpoint= ' + approveEndpoint); const rejectEndpoint = apigwEndpint + "/execution?action=reject&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('rejectEndpoint= ' + rejectEndpoint); const emailSnsTopic = "${SNSHumanApprovalEmailTopic}"; console.log('emailSnsTopic= ' + emailSnsTopic); var emailMessage = 'Welcome! \n\n'; emailMessage += 'This is an email requiring an approval for a step functions execution. \n\n' emailMessage += 'Please check the following information and click "Approve" link if you want to approve. \n\n' emailMessage += 'Execution Name -> ' + executionName + '\n\n' emailMessage += 'Approve ' + approveEndpoint + '\n\n' emailMessage += 'Reject ' + rejectEndpoint + '\n\n' emailMessage += 'Thanks for using Step functions!' const sns = new AWS.SNS(); var params = { Message: emailMessage, Subject: "Required approval from AWS Step Functions", TopicArn: emailSnsTopic }; sns.publish(params) .promise() .then(function(data) { console.log("MessageID is " + data.MessageId); callback(null); }).catch( function(err) { console.error(err, err.stack); callback(err); }); } LambdaSendEmailExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: SNSSendEmailPolicy PolicyDocument: Statement: - Effect: Allow Action: - "SNS:Publish" Resource: - !Sub "${SNSHumanApprovalEmailTopic}" #S3 Vending Machine S3Templates: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 AccessControl: BucketOwnerFullControl PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true VersioningConfiguration: Status: Enabled S3BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3Templates PolicyDocument: Statement: - Sid: GetObjectAccess Action: - s3:GetObject Effect: Allow Resource: - !Join - '' - - !GetAtt "S3Templates.Arn" - '/*' #Principal: # AWS: # - '123456789012' # To work without AWS Organization, Replace with a valid source AWS Account Id and remove the next 4 lines Principal: '*' Condition: StringEquals: 'aws:PrincipalOrgID': !Ref OrganizationId # End state machine that publishes to Lambda and sends an email with the link for approval Outputs: ApiGatewayInvokeURL: Value: !Sub "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" S3VendingMachineSiteBucket: Value: !GetAtt CloudfrontAuthorizationEdge.Outputs.S3Bucket VendingMachineUrl: Value: !GetAtt CloudfrontAuthorizationEdge.Outputs.WebsiteUrl CloudFrontDistributionId: Value: !GetAtt CloudfrontAuthorizationEdge.Outputs.CloudFrontDistribution S3VendingMachineTemplatesBucket: Value: Ref: S3Templates #sc - products