--- AWSTemplateFormatVersion: "2010-09-09" Description: This AWS CloudFormation Template configures an environment with the necessary resources to support the Serverless Round of the Identity Round-Robin Workshop. Parameters: ResourcePrefix: Type: String Default: identity-wksp-serverless AllowedValues: - identity-wksp-serverless Description: Prefix of Resources created for this workshop. Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Resource Configuration" Parameters: - ResourcePrefix ParameterLabels: ResourcePrefix: default: "Prefix" Mappings: Application: bucket: "name": "sa-security-specialist-workshops-us-west-2" prefix: "key": "identity-workshop/serverless/website/" Resources: # Website Bucket Resources WebsiteBucket: Type: 'AWS::S3::Bucket' Properties: BucketName: Fn::Join: - '-' - [!Ref ResourcePrefix, !Ref "AWS::AccountId", !Ref "AWS::Region",'wildrydes'] WebsiteConfiguration: IndexDocument: index.html WebsiteBucketPolicy: Type: "AWS::S3::BucketPolicy" Properties: Bucket: !Ref WebsiteBucket PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: "*" Action: - s3:GetObject - s3:GetObjectVersionAcl - s3:GetObjectVersionTagging - s3:PutObjectAcl - s3:PutObjectTagging - s3:DeleteObject Resource: !Sub "arn:aws:s3:::${WebsiteBucket}/*" # Resources to download and upload website content WebsiteContent: DependsOn: - WebsiteBucket Type: "Custom::S3Objects" Properties: ServiceToken: !GetAtt WebsiteContentLambda.Arn SourceBucket: !FindInMap [Application, bucket, name] SourcePrefix: !FindInMap [Application, prefix, key] Bucket: !Ref WebsiteBucket WebsiteContentLambdaRole: Type: AWS::IAM::Role Properties: RoleName: Fn::Join: - '-' - [!Ref ResourcePrefix, 'lambda-content'] AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: S3Access PolicyDocument: Version: 2012-10-17 Statement: - Sid: AllowLogging Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*" - Sid: SourceBucketReadAccess Effect: Allow Action: - "s3:ListBucket" - "s3:GetObject" Resource: - Fn::Join: - '' - ['arn:aws:s3:::', !FindInMap [Application, bucket, name]] - Fn::Join: - '' - ['arn:aws:s3:::', !FindInMap [Application, bucket, name], '/', !FindInMap [Application, prefix, key],'*'] - Sid: DestBucketWriteAccess Effect: Allow Action: - "s3:ListBucket" - "s3:GetObject" - "s3:PutObject" - "s3:PutObjectAcl" - "s3:PutObjectVersionAcl" - "s3:DeleteObject" - "s3:DeleteObjectVersion" - "s3:CopyObject" Resource: - !Sub "arn:aws:s3:::${WebsiteBucket}" - !Sub "arn:aws:s3:::${WebsiteBucket}/*" WebsiteContentLambda: Type: AWS::Lambda::Function Properties: FunctionName: Fn::Join: - '-' - [!Ref ResourcePrefix, 'content'] Description: Copies objects from a source S3 bucket to a destination S3 bucket. Handler: index.handler Runtime: python3.9 Role: !GetAtt WebsiteContentLambdaRole.Arn Timeout: 120 Code: ZipFile: | from botocore.exceptions import ClientError import os import json import cfnresponse import boto3 import logging # Create S3 clinet s3 = boto3.client('s3') # Set Logging logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): logger.info("Received event: %s" % json.dumps(event)) # Set Variables source_bucket = event['ResourceProperties']['SourceBucket'] source_prefix = event['ResourceProperties'].get('SourcePrefix') or '' bucket = event['ResourceProperties']['Bucket'] prefix = event['ResourceProperties'].get('Prefix') or '' # Set Initial CFN Response result = cfnresponse.SUCCESS try: # Create or Update Event if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': paginator = s3.get_paginator('list_objects_v2') page_iterator = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix) for key in {x['Key'] for page in page_iterator for x in page['Contents']}: dest_key = os.path.join(prefix, os.path.relpath(key, source_prefix)) if not key.endswith('/'): print('copy {} to {}'.format(key, dest_key)) s3.copy_object(CopySource={'Bucket': source_bucket, 'Key': key}, Bucket=bucket, Key = dest_key, ACL='public-read') result = cfnresponse.SUCCESS # Delete Event elif event['RequestType'] == 'Delete': paginator = s3.get_paginator('list_objects_v2') page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix) objects = [{'Key': x['Key']} for page in page_iterator for x in page['Contents']] s3.delete_objects(Bucket=bucket, Delete={'Objects': objects}) result = cfnresponse.SUCCESS except ClientError as e: logger.error('Error: %s', e) result = cfnresponse.FAILED cfnresponse.send(event, context, result, {}) # CloudFront distribution UnicornOAI: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Unicorn OAI UnicornDistribution: DependsOn: - UnicornOAI - WebsiteContent Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Origins: - DomainName: Fn::Join: - '' - [!Ref WebsiteBucket, '.s3.amazonaws.com'] Id: Fn::Join: - '-' - [!Ref ResourcePrefix, 'S3-Unicorn'] S3OriginConfig: OriginAccessIdentity: Fn::Join: - '' - ['origin-access-identity/cloudfront/', !Ref UnicornOAI] Enabled: 'true' Comment: Distribution for the WildRydes web application DefaultRootObject: index.html DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT TargetOriginId: Fn::Join: - '-' - [!Ref ResourcePrefix, 'S3-Unicorn'] ForwardedValues: QueryString: 'false' Cookies: Forward: none ViewerProtocolPolicy: https-only DefaultTTL: 0 MaxTTL: 0 MinTTL: 0 PriceClass: PriceClass_100 ViewerCertificate: CloudFrontDefaultCertificate: 'true' # User Pool and Client UserPool: DependsOn: - WebsiteContent Type: AWS::Cognito::UserPool Properties: UserPoolName: WildRydes Policies: PasswordPolicy: RequireLowercase: 'false' RequireSymbols: 'false' RequireNumbers: 'false' MinimumLength: 6 RequireUppercase: 'false' AdminCreateUserConfig: AllowAdminCreateUserOnly: 'true' AliasAttributes: - email Schema: - Name: email AttributeDataType: String Mutable: false Required: true UserPoolClient: DependsOn: - UserPool Type: AWS::Cognito::UserPoolClient Properties: ClientName: WildRydesWeb UserPoolId: !Ref UserPool GenerateSecret: false # Resources to update website content WebsiteContentUpdate: DependsOn: - UserPoolClient Type: "Custom::ConfigFile" Properties: ServiceToken: !GetAtt WebsiteContentUpdateLambda.Arn UserPool: !Ref UserPool Client: !Ref UserPoolClient Region: !Ref "AWS::Region" Bucket: !Ref WebsiteBucket WebsiteContentUpdateLambdaRole: Type: AWS::IAM::Role Properties: RoleName: Fn::Join: - '-' - [!Ref ResourcePrefix, 'lambda-content-update'] AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CognitoConfig PolicyDocument: Version: 2012-10-17 Statement: - Sid: Logging Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*" - Sid: ConfigBucketWriteAccess Effect: Allow Action: - "s3:PutObject" - "s3:PutObjectAcl" - "s3:PutObjectVersionAcl" Resource: - !Sub "arn:aws:s3:::${WebsiteBucket}/*" WebsiteContentUpdateLambda: Type: AWS::Lambda::Function Properties: FunctionName: Fn::Join: - '-' - [!Ref ResourcePrefix, 'content-update'] Description: Updates object on S3 for client-side configuration Handler: index.handler Runtime: python3.9 Role: !GetAtt WebsiteContentUpdateLambdaRole.Arn Timeout: 120 Code: ZipFile: | from botocore.exceptions import ClientError import json import boto3 import cfnresponse import logging # Create S3 resource s3 = boto3.resource('s3') # Set Logging logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): logger.info("Received event: %s" % json.dumps(event)) # Set Variables userPoolId = event['ResourceProperties']['UserPool'] clientId = event['ResourceProperties']['Client'] region = event['ResourceProperties']['Region'] bucket = event['ResourceProperties']['Bucket'] # Set Initial CFN Response result = cfnresponse.SUCCESS try: if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': # Set config file config_content = """ var _config = { cognito: { userPoolId: '%s', // e.g. us-east-2_uXboG5pAb userPoolClientId: '%s', // e.g. 25ddkmj4v6hfsfvruhpfi7n4hv region: '%s', // e.g. us-east-2 }, api: { invokeUrl: '', // Base URL of your API including the stage, e.g. 'https://rc7nyt4tql.execute-api.us-west-2.amazonaws.com/prod' } }; """ # Update config file with the necessary resources config_content = config_content % (userPoolId, clientId, region) # Update the file in the S3 bucket config = s3.Object(bucket,'js/config.js') config.put(Body=config_content) result = cfnresponse.SUCCESS elif event['RequestType'] == 'Delete': result = cfnresponse.SUCCESS except ClientError as e: logger.error('Error: %s', e) result = cfnresponse.FAILED cfnresponse.send(event, context, result, {}) Outputs: WebsiteS3URL: Value: !GetAtt WebsiteBucket.WebsiteURL WebsiteCloudFrontURL: Value: Fn::Join: - '' - ['https://', !GetAtt UnicornDistribution.DomainName]