From 417dd15e6bd16ac165fb7d192a6be6e05c6b689c Mon Sep 17 00:00:00 2001 From: Lee Hsiang Date: Thu, 6 Jul 2023 17:36:47 +0800 Subject: [PATCH] Fixed issues 2, 6, 18, 19, 20, 21 --- .gitignore | 2 +- src/entitlement-sqs.js | 8 +- src/grant-revoke-access-to-product.js | 1 - src/lambda-edge/edge-redirect.js | 22 -- src/lambda-edge/package.json | 4 - src/metering-hourly-job.js | 9 +- src/metering-sqs.js | 8 +- src/redirect.js | 15 ++ src/register-new-subscriber.js | 11 +- src/subscription-sqs.js | 6 +- template.yaml | 364 +++++++++++++++++--------- 11 files changed, 280 insertions(+), 170 deletions(-) delete mode 100644 src/lambda-edge/edge-redirect.js delete mode 100644 src/lambda-edge/package.json create mode 100644 src/redirect.js diff --git a/.gitignore b/.gitignore index f5c66aa..75cb3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ node_modules .DS_Store packaged.yaml deploy.sh -samconfig.toml +*.toml diff --git a/src/entitlement-sqs.js b/src/entitlement-sqs.js index 6858037..20199f2 100644 --- a/src/entitlement-sqs.js +++ b/src/entitlement-sqs.js @@ -1,8 +1,8 @@ const AWS = require('aws-sdk'); - -const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'us-east-1' }); +const { NewSubscribersTableName: newSubscribersTableName, AWS_REGION: aws_region } = process.env; +// MarketplaceEntitlementService is instantianise only in the us-east-1 https://docs.aws.amazon.com/general/latest/gr/aws-marketplace.html#marketplaceentitlement const marketplaceEntitlementService = new AWS.MarketplaceEntitlementService({ apiVersion: '2017-01-11', region: 'us-east-1' }); -const { NewSubscribersTableName: newSubscribersTableName } = process.env; +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: aws_region }); exports.handler = async (event) => { await Promise.all(event.Records.map(async (record) => { @@ -47,7 +47,5 @@ exports.handler = async (event) => { throw new Error(`Unhandled action - msg: ${JSON.stringify(record)}`); } })); - - return {}; }; diff --git a/src/grant-revoke-access-to-product.js b/src/grant-revoke-access-to-product.js index 5444938..fe3b10a 100644 --- a/src/grant-revoke-access-to-product.js +++ b/src/grant-revoke-access-to-product.js @@ -1,6 +1,5 @@ const winston = require('winston'); const AWS = require('aws-sdk'); - const SNS = new AWS.SNS({ apiVersion: '2010-03-31' }); const { SupportSNSArn: TopicArn } = process.env; const logger = winston.createLogger({ diff --git a/src/lambda-edge/edge-redirect.js b/src/lambda-edge/edge-redirect.js deleted file mode 100644 index c6c465f..0000000 --- a/src/lambda-edge/edge-redirect.js +++ /dev/null @@ -1,22 +0,0 @@ -exports.lambdaHandler = async (event) => { - const { request } = event.Records[0].cf; - - const redirect = request.method === 'POST' && request.body.data; - - - if (redirect) { - const body = Buffer.from(request.body.data, 'base64').toString(); - return { - status: '302', - statusDescription: 'Found', - headers: { - location: [{ - key: 'Location', - value: `/?${body}`, - }], - }, - }; - } - - return request; -}; diff --git a/src/lambda-edge/package.json b/src/lambda-edge/package.json deleted file mode 100644 index 3241313..0000000 --- a/src/lambda-edge/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "lambda-edge-function", - "version": "1.0.0" -} diff --git a/src/metering-hourly-job.js b/src/metering-hourly-job.js index a45c6c9..ded3633 100644 --- a/src/metering-hourly-job.js +++ b/src/metering-hourly-job.js @@ -1,9 +1,8 @@ const AWS = require('aws-sdk'); - -const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'us-east-1' }); -const sqs = new AWS.SQS({ apiVersion: '2012-11-05', region: 'us-east-1' }); - -const { SQSMeteringRecordsUrl: QueueUrl, AWSMarketplaceMeteringRecordsTableName } = process.env; +const { AWS_REGION: aws_region } = process.env; +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: aws_region }); +const sqs = new AWS.SQS({ apiVersion: '2012-11-05', region: aws_region }); +const { SQSMeteringRecordsUrl: QueueUrl, AWSMarketplaceMeteringRecordsTableName: AWSMarketplaceMeteringRecordsTableName } = process.env; async function asyncForEach(array, callback) { diff --git a/src/metering-sqs.js b/src/metering-sqs.js index 5f33750..be6815f 100644 --- a/src/metering-sqs.js +++ b/src/metering-sqs.js @@ -1,16 +1,14 @@ const AWS = require('aws-sdk'); - -const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'us-east-1' }); +const { ProductCode: ProductCode, AWSMarketplaceMeteringRecordsTableName: AWSMarketplaceMeteringRecordsTableName , AWS_REGION: aws_region } = process.env; +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: aws_region }); +// MarketplaceMetering is instantianize in us-east-1 as all SaaS product listing ARN is stored in us-east-1. const marketplacemetering = new AWS.MarketplaceMetering({ apiVersion: '2016-01-14', region: 'us-east-1' }); -const { ProductCode, AWSMarketplaceMeteringRecordsTableName } = process.env; - exports.handler = async (event) => { await Promise.all(event.Records.map(async (record) => { const body = JSON.parse(record.body); console.log(`SQS message body: ${record.body}`); - const timestmpNow = new Date(); const UsageRecords = []; diff --git a/src/redirect.js b/src/redirect.js new file mode 100644 index 0000000..d8001cd --- /dev/null +++ b/src/redirect.js @@ -0,0 +1,15 @@ +const { RedirectUrl: landingPageUrl } = process.env; + +exports.redirecthandler = async(event, context, callback) => { + + const redirectUrl = landingPageUrl + "?" + event['body']; + const response = { + statusCode: 302, + headers: { + Location: redirectUrl + }, + }; + + return response; + +}; diff --git a/src/register-new-subscriber.js b/src/register-new-subscriber.js index b922d64..16c4170 100644 --- a/src/register-new-subscriber.js +++ b/src/register-new-subscriber.js @@ -1,9 +1,10 @@ const AWS = require('aws-sdk'); -const ses = new AWS.SES({ region: "us-east-1" }); -const marketplacemetering = new AWS.MarketplaceMetering({ apiVersion: '2016-01-14', region: 'us-east-1' }); -const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'us-east-1' }); -const sqs = new AWS.SQS({ apiVersion: '2012-11-05', region: 'us-east-1' }); -const { NewSubscribersTableName: newSubscribersTableName, EntitlementQueueUrl: entitlementQueueUrl, MarketplaceSellerEmail: marketplaceSellerEmail } = process.env; +const { NewSubscribersTableName: newSubscribersTableName, EntitlementQueueUrl: entitlementQueueUrl, MarketplaceSellerEmail: marketplaceSellerEmail, AWS_REGION:aws_region } = process.env; +const ses = new AWS.SES({ region: aws_region}); +// Require confirmation on whether this has dependency on the marketplace seller account origin. +const marketplacemetering = new AWS.MarketplaceMetering({ apiVersion: '2016-01-14', region: aws_region }); +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: aws_region }); +const sqs = new AWS.SQS({ apiVersion: '2012-11-05', region: aws_region }); const lambdaResponse = (statusCode, body) => ({ statusCode, diff --git a/src/subscription-sqs.js b/src/subscription-sqs.js index 836dbee..694fb02 100644 --- a/src/subscription-sqs.js +++ b/src/subscription-sqs.js @@ -1,8 +1,8 @@ const AWS = require('aws-sdk'); - -const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'us-east-1' }); +const { SupportSNSArn: TopicArn, NewSubscribersTableName: newSubscribersTableName, AWS_REGION: aws_region } = process.env; +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: aws_region }); const SNS = new AWS.SNS({ apiVersion: '2010-03-31' }); -const { SupportSNSArn: TopicArn, NewSubscribersTableName: newSubscribersTableName } = process.env; + exports.SQSHandler = async (event) => { await Promise.all(event.Records.map(async (record) => { diff --git a/template.yaml b/template.yaml index 969a324..a0b5578 100644 --- a/template.yaml +++ b/template.yaml @@ -14,8 +14,10 @@ Globals: AllowCredentials: "'*'" Parameters: + WebsiteS3BucketName: Type: String + #AllowedPattern: "(?!(^((2(5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2})\\.){3}(2(5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2})$|^xn--|.+-s3alias$))^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$" Default: "" NewSubscribersTableName: @@ -49,13 +51,20 @@ Parameters: AllowedPattern: ".*" Default: "" - EntitlementSNSTopic: + CreateCrossAccountRole: + Default: false + Description: "Do you intend to use cross account access with this integration core?" + Type: String + AllowedValues: [true, false] + + CrossAccountId: + Default: '' + Description: "Enter the cross AWS account id" Type: String - Default: "" - SubscriptionSNSTopic: + CrossAccountRoleName: Type: String - Default: "" + Description: "Your Role Name (ex: OrganizationAccountAccessRole); This will need to be the same across all of the Member Accounts" CreateRegistrationWebPage: Default: true @@ -75,114 +84,56 @@ Conditions: CreateWeb: !Equals [!Ref CreateRegistrationWebPage, true] Buyernotificationemail: !Not [!Equals [!Ref MarketplaceSellerEmail, ""]] + CreateCrossAccount: !Equals [!Ref CreateCrossAccountRole, true] -Resources: - CloudFrontOriginAccessIdentity: - Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity" - Condition: CreateWeb - Properties: - CloudFrontOriginAccessIdentityConfig: - Comment: "Serverless website OA" - - CloudfrontDistribution: - Type: "AWS::CloudFront::Distribution" - Condition: CreateWeb - Properties: - DistributionConfig: - Comment: "Cloudfront distribution for serverless website" - DefaultRootObject: "index.html" - Enabled: true - HttpVersion: http2 - # List of origins that Cloudfront will connect to - Origins: - - Id: s3-website - DomainName: !GetAtt WebsiteS3Bucket.DomainName - S3OriginConfig: - # Restricting Bucket access through an origin access identity - OriginAccessIdentity: - Fn::Sub: "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - # To connect the CDN to the origins you need to specify behaviours - DefaultCacheBehavior: - # Compress resources automatically ( gzip ) - Compress: true - AllowedMethods: - - HEAD - - DELETE - - POST - - GET - - OPTIONS - - PUT - - PATCH - ForwardedValues: - QueryString: false - LambdaFunctionAssociations: - - EventType: viewer-request - LambdaFunctionARN: !Ref LambdaEdgeRedirectPostRequests.Version - IncludeBody: true - TargetOriginId: s3-website - ViewerProtocolPolicy: redirect-to-https - Logging: - Bucket: !GetAtt WebsiteS3BucketLog.DomainName - IncludeCookies: false - Prefix: "access-logs" - - WebsiteS3Bucket: - Type: AWS::S3::Bucket - Condition: CreateWeb - Properties: - BucketName: !Ref WebsiteS3BucketName - WebsiteS3BucketLog: - Type: AWS::S3::Bucket - Condition: CreateWeb - Properties: - BucketName: !Join ["-", [!Ref WebsiteS3BucketName, "log"]] - OwnershipControls: - Rules: - - ObjectOwnership: BucketOwnerPreferred - IntelligentTieringConfigurations: - - Id: !Join ["-", [!Ref WebsiteS3BucketName, "log"]] - Status: Enabled - Tierings: - - AccessTier: ARCHIVE_ACCESS - Days: 90 +Resources: - S3BucketPolicy: - Type: AWS::S3::BucketPolicy - Condition: CreateWeb + ServerlessApi: + Type: AWS::Serverless::Api Properties: - Bucket: !Ref WebsiteS3Bucket - PolicyDocument: - # Restricting access to cloudfront only. - Statement: - - Effect: Allow - Action: "s3:GetObject" - Resource: - - !Sub "arn:aws:s3:::${WebsiteS3Bucket}/*" - Principal: - AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}" - - LambdaEdgeRedirectPostRequests: - Type: AWS::Serverless::Function - Condition: CreateWeb - UpdateReplacePolicy: Delete - DeletionPolicy: Delete + StageName: Prod + MethodSettings: + - ResourcePath: /subscriber + HttpMethod: POST, OPTIONS + - ResourcePath: /redirectmarketplacetoken + HttpMethod: POST, OPTIONS + + CrossAccountRoleForSaaSIntegration: + Type: AWS::IAM::Role + Condition: CreateCrossAccount + DependsOn: + - AWSMarketplaceMeteringRecords + - AWSMarketplaceSubscribers Properties: - Runtime: nodejs18.x - CodeUri: src/lambda-edge/ - Handler: edge-redirect.lambdaHandler - Timeout: 5 - AutoPublishAlias: live + RoleName: !Join ["-", [!Ref "AWS::StackName", !Ref CrossAccountRoleName]] AssumeRolePolicyDocument: - Version: "2012-10-17" + Version: 2012-10-17 Statement: - - Effect: "Allow" - Action: "sts:AssumeRole" + - Effect: Allow Principal: - Service: - - "lambda.amazonaws.com" - - "edgelambda.amazonaws.com" - + AWS: + - !Join [":", ["arn:aws:iam:", !Ref CrossAccountId , "root"]] + Action: + - 'sts:AssumeRole' + Condition: + StringEquals: + 'sts:ExternalId': !Ref CrossAccountRoleName + Path: / + Policies: + - PolicyName: !Join ["-", [!Ref "AWS::StackName", "CrossAccountPolicy"]] + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - 'dynamodb:PutItem' + - 'dynamodb:DeleteItem' + - 'dynamodb:UpdateItem' + Resource: + - !GetAtt AWSMarketplaceMeteringRecords.Arn + - !GetAtt AWSMarketplaceSubscribers.Arn + AWSMarketplaceMeteringRecords: Type: AWS::DynamoDB::Table Condition: CreateSubscriptionLogic @@ -223,7 +174,7 @@ Resources: TableName: !Ref NewSubscribersTableName StreamSpecification: StreamViewType: NEW_AND_OLD_IMAGES - + RegisterNewMarketplaceCustomer: Type: AWS::Serverless::Function Properties: @@ -234,13 +185,9 @@ Resources: Variables: NewSubscribersTableName: !Ref NewSubscribersTableName EntitlementQueueUrl: - !If [CreateEntitlementLogic, !Ref EntitlementSQSQueue, ""] + !If [CreateEntitlementLogic, !Ref EntitlementSQSQueue, !Ref AWS::NoValue] MarketplaceSellerEmail: - Fn::If: - - Buyernotificationemail - - Ref: MarketplaceSellerEmail - - Ref: AWS::NoValue - + !If [Buyernotificationemail, !Ref MarketplaceSellerEmail, !Ref AWS::NoValue] Policies: - DynamoDBWritePolicy: TableName: !Ref NewSubscribersTableName @@ -272,6 +219,7 @@ Resources: Properties: Path: /subscriber Method: post + RestApiId: !Ref ServerlessApi EntitlementSQSQueue: Type: AWS::SQS::Queue @@ -302,7 +250,8 @@ Resources: MySQSEvent: Type: SNS Properties: - Topic: !Ref EntitlementSNSTopic + Topic: {"Fn::Join" : ["", ["arn:aws:sns:us-east-1:287250355862:aws-mp-entitlement-notification-", !Ref ProductCode]]} + Region: us-east-1 SqsSubscription: BatchSize: 1 QueueArn: !GetAtt EntitlementSQSQueue.Arn @@ -331,7 +280,9 @@ Resources: MySQSEvent: Type: SNS Properties: - Topic: !Ref SubscriptionSNSTopic + #Topic: !Ref SubscriptionSNSTopic + Topic: {"Fn::Join" : ["", ["arn:aws:sns:us-east-1:287250355862:aws-mp-subscription-notification-", !Ref ProductCode]]} + Region: us-east-1 SqsSubscription: true SupportSNSTopic: @@ -427,16 +378,191 @@ Resources: Properties: Queue: !GetAtt SQSMeteringRecords.Arn BatchSize: 1 + + + Bucket: !GetAtt WebsiteS3BucketLog.DomainName + IncludeCookies: false + Prefix: "access-logs" + + WebsiteS3Bucket: + Type: AWS::S3::Bucket + Condition: CreateWeb + Properties: + BucketName: !Ref WebsiteS3BucketName + + WebsiteS3BucketLog: + Type: AWS::S3::Bucket + Condition: CreateWeb + Properties: + BucketName: !Join ["-", [!Ref WebsiteS3BucketName, "log"]] + OwnershipControls: + Rules: + - ObjectOwnership: BucketOwnerPreferred + IntelligentTieringConfigurations: + - Id: !Join ["-", [!Ref WebsiteS3BucketName, "log"]] + Status: Enabled + Tierings: + - AccessTier: ARCHIVE_ACCESS + Days: 90 + + S3BucketPolicy: + Type: AWS::S3::BucketPolicy + Condition: CreateWeb + Properties: + Bucket: !Ref WebsiteS3Bucket + PolicyDocument: + # Restricting access to cloudfront only. + Statement: + - Effect: Allow + Action: "s3:GetObject" + Resource: + - !Sub "arn:aws:s3:::${WebsiteS3Bucket}/*" + Principal: + AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}" + + LambdaRedirectPostRequests: + Type: AWS::Serverless::Function + Condition: CreateWeb + Properties: + Runtime: nodejs18.x + CodeUri: src/ + Handler: redirect.redirecthandler + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: "sts:AssumeRole" + Principal: + Service: + - "lambda.amazonaws.com" + Environment: + Variables: + RedirectUrl: https://aws-ia.github.io/cloudformation-aws-marketplace-saas/#_post_deployment_steps + Events: + RedirectMarketplaceToken: + Type: Api + Properties: + Path: /redirectmarketplacetoken + Method: post + RestApiId: !Ref ServerlessApi + + CloudFrontOriginAccessIdentity: + Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity" + Condition: CreateWeb + Properties: + CloudFrontOriginAccessIdentityConfig: + Comment: !Sub "Serverless website OA - ${AWS::StackName}" + + CloudfrontDistribution: + Type: "AWS::CloudFront::Distribution" + Condition: CreateWeb + DependsOn: ServerlessApi + Properties: + DistributionConfig: + Comment: !Sub "Cloudfront distribution for serverless website - ${AWS::StackName}" + DefaultRootObject: "index.html" + Enabled: true + HttpVersion: http2 + # List of origins that Cloudfront will connect to + Origins: + - Id: s3-website + DomainName: !GetAtt WebsiteS3Bucket.DomainName + S3OriginConfig: + # Restricting Bucket access through an origin access identity + OriginAccessIdentity: + !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" + - Id: api-gateway + DomainName: !Sub "${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com" + CustomOriginConfig: + OriginProtocolPolicy: "https-only" + OriginSSLProtocols: + - "TLSv1.2" + HTTPSPort: 443 + OriginPath: "/Prod" + # To connect the CDN to the origins you need to specify behaviours + DefaultCacheBehavior: + # Compress resources automatically ( gzip ) + Compress: true + AllowedMethods: + - HEAD + - DELETE + - POST + - GET + - OPTIONS + - PUT + - PATCH + ForwardedValues: + QueryString: false + TargetOriginId: s3-website + ViewerProtocolPolicy: redirect-to-https + CacheBehaviors: + - PathPattern: /redirectmarketplacetoken + AllowedMethods: + - HEAD + - DELETE + - POST + - GET + - OPTIONS + - PUT + - PATCH + TargetOriginId: api-gateway + ViewerProtocolPolicy: redirect-to-https + ResponseHeadersPolicyId: 60669652-455b-4ae9-85a4-c4c02393f86c + CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad + OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac + - PathPattern: /subscriber + AllowedMethods: + - HEAD + - DELETE + - POST + - GET + - OPTIONS + - PUT + - PATCH + TargetOriginId: api-gateway + ViewerProtocolPolicy: redirect-to-https + ResponseHeadersPolicyId: 60669652-455b-4ae9-85a4-c4c02393f86c + CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad + OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac + Logging: + Bucket: !GetAtt WebsiteS3BucketLog.DomainName + IncludeCookies: false + Prefix: "access-logs" + + Outputs: - APIUrl: - Description: API gateway URL to replace baseUrl value in web/script.js - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" + BaseUrl: + Description: URL to replace baseUrl value in web/script.js. + Value: + !If [ + CreateWeb, + !Sub "https://${CloudfrontDistribution.DomainName}/", + !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" + ] + + MarketplaceFulfillmentUrl: + Description: This is the marketplace fulfillment url. + Value: + !If [ + CreateWeb, + !Sub "https://${CloudfrontDistribution.DomainName}/redirectmarketplacetoken", + !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/redirectmarketplacetoken" + ] + + CrossAccountRole: + Description: This is the cross account role ARN. + Value: + !If [ + CreateCrossAccount, + !GetAtt CrossAccountRoleForSaaSIntegration.Arn, + "N/A" + ] LandingPageUrl: - Description: URL to access your landing page and update SaaS URL field in your listing. + Description: URL to access your landing page. Value: !If [ CreateWeb, !Sub "https://${CloudfrontDistribution.DomainName}/index.html", - "N/A", - ] \ No newline at end of file + "N/A" + ]