From 3d2dddb481579e8759d96d879c38ed9eb10c0797 Mon Sep 17 00:00:00 2001 From: Kurt Gardiner Date: Fri, 10 Jan 2020 12:03:57 +1100 Subject: [PATCH] Replaces Cognito custom lambdas with native cfn --- cloudformation/template.yaml | 124 ++---------------- .../index.js | 53 -------- .../notify-cfn.js | 108 --------------- .../cfn-cognito-user-pools-domain/index.js | 89 ------------- .../notify-cfn.js | 108 --------------- 5 files changed, 9 insertions(+), 473 deletions(-) delete mode 100644 lambdas/cfn-cognito-user-pools-client-settings/index.js delete mode 100644 lambdas/cfn-cognito-user-pools-client-settings/notify-cfn.js delete mode 100644 lambdas/cfn-cognito-user-pools-domain/index.js delete mode 100644 lambdas/cfn-cognito-user-pools-domain/notify-cfn.js diff --git a/cloudformation/template.yaml b/cloudformation/template.yaml index 2495c9adc..e4addabe3 100644 --- a/cloudformation/template.yaml +++ b/cloudformation/template.yaml @@ -72,7 +72,7 @@ Parameters: StaticAssetRebuildMode: Type: String - Description: By default, a static asset rebuild doesn't overwrite custom-content. Provide the value `overwrite-content` to replace the custom-content with your local version. Don't do this unless you know what you're doing -- all custom changes in your s3 bucket will be lost. + Description: By default, a static asset rebuild doesn't overwrite custom-content. Provide the value `overwrite-content` to replace the custom-content with your local version. Don't do this unless you know what you're doing -- all custom changes in your s3 bucket will be lost. Default: '' AllowedValues: - 'overwrite-content' @@ -112,7 +112,7 @@ Parameters: Type: String Description: Only applicable if creating a custom domain name for your dev portal. Defaults to false, and you'll need to provide your own nameserver hosting. If set to true, a Route53 HostedZone and RecordSet are created for you. Default: 'false' - AllowedValues: + AllowedValues: - 'false' - 'true' ConstraintDescription: Malformed input - Parameter UseRoute53Nameservers value must be either 'true' or 'false' @@ -121,7 +121,7 @@ Parameters: Type: String Description: Enabling this weakens security features (OAI, SSL, site S3 bucket with public read ACLs, Cognito callback verification, CORS, etc.) for easier development. It also breaks frontend routing (except to /index.html), including deep linking and page refresh. Do not enable this in production! Additionally, do not update a stack that was previously in development mode to be a production stack; instead, make a new stack that has never been in development mode. Default: 'false' - AllowedValues: + AllowedValues: - 'false' - 'true' ConstraintDescription: Malformed input - Parameter DevelopmentMode value must be either 'true' or 'false' @@ -805,7 +805,7 @@ Resources: - Effect: Allow Action: - s3:ListBucket - Resource: + Resource: - !Join - '' - - 'arn:aws:s3:::' @@ -1059,67 +1059,13 @@ Resources: CognitoUserPoolClient: Type: AWS::Cognito::UserPoolClient - # It's really unintuitive, but changing any of the properties here will cause stack updates to deploy non-functionally. - # The CognitoUserPoolClientSettings custom resource runs after this resource and adds a bunch of fields. - # However, when this is updated and changes, the CUPCS custom resource doesn't re-run, and so a bunch of vital - # settings won't be set, e.g., CallbackURL. Properties: UserPoolId: !Ref CognitoUserPool ClientName: CognitoIdentityPool GenerateSecret: false RefreshTokenValidity: 30 - - CognitoUserPoolClientSettingsBackingFnRole: - Type: 'AWS::IAM::Role' - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: 'sts:AssumeRole' - Principal: - Service: lambda.amazonaws.com - Policies: - - PolicyName: WriteCloudWatchLogs - PolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: - - 'logs:CreateLogGroup' - - 'logs:CreateLogStream' - - 'logs:PutLogEvents' - Resource: 'arn:aws:logs:*:*:*' - - PolicyName: UpdateUserPoolClient - PolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: 'cognito-idp:UpdateUserPoolClient' - Resource: 'arn:aws:cognito-idp:*:*:userpool/*' - - CognitoUserPoolClientSettingsBackingFn: - Type: AWS::Serverless::Function - Properties: - Runtime: nodejs12.x - MemorySize: 128 - Timeout: 300 - CodeUri: ../lambdas/cfn-cognito-user-pools-client-settings - Handler: index.handler - Role: !GetAtt CognitoUserPoolClientSettingsBackingFnRole.Arn - - CognitoUserPoolClientSettings: - Type: AWS::CloudFormation::CustomResource - Properties: - Timeout: 360 - ServiceToken: !GetAtt CognitoUserPoolClientSettingsBackingFn.Arn - UserPoolId: !Ref CognitoUserPool - UserPoolClientId: !Ref CognitoUserPoolClient SupportedIdentityProviders: [ "COGNITO" ] # should (eventually) allow people to add values - CallbackURL: !If [ DevelopmentMode, + CallbackURLs: !If [ DevelopmentMode, [ 'http://localhost:3000/index.html?action=login', !Join [ '', [ 'https://', !GetAtt DevPortalSiteS3Bucket.RegionalDomainName, '/index.html?action=login' ]] @@ -1128,7 +1074,7 @@ Resources: !Join [ '', [ 'https://', !If [ UseCustomDomainName, !Ref CustomDomainName, !GetAtt DefaultCloudfrontDistribution.DomainName ], '/index.html?action=login' ]] ] ] - LogoutURL: !If [ DevelopmentMode, + LogoutURLs: !If [ DevelopmentMode, [ 'http://localhost:3000/index.html?action=logout', !Join [ '', [ 'https://', !GetAtt DevPortalSiteS3Bucket.RegionalDomainName, '/index.html?action=logout' ]] @@ -1141,64 +1087,12 @@ Resources: AllowedOAuthFlows: [ "implicit" ] AllowedOAuthScopes: [ "openid" ] - CognitoUserPoolDomainBackingFnRole: - Type: 'AWS::IAM::Role' - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: 'sts:AssumeRole' - Principal: - Service: lambda.amazonaws.com - Policies: - - PolicyName: WriteCloudWatchLogs - PolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: - - 'logs:CreateLogGroup' - - 'logs:CreateLogStream' - - 'logs:PutLogEvents' - Resource: 'arn:aws:logs:*:*:*' - - PolicyName: ManageUserPoolDomain - PolicyDocument: - Version: '2012-10-17' - Statement: - - - Effect: Allow - Action: 'cognito-idp:CreateUserPoolDomain' - Resource: 'arn:aws:cognito-idp:*:*:userpool/*' - - - Effect: Allow - Action: 'cognito-idp:DeleteUserPoolDomain' - Resource: 'arn:aws:cognito-idp:*:*:userpool/*' - - - Effect: Allow - Action: 'cognito-idp:DescribeUserPoolDomain' - Resource: '*' - - CognitoUserPoolDomainBackingFn: - Type: AWS::Serverless::Function - Properties: - Runtime: nodejs12.x - MemorySize: 128 - Timeout: 300 - CodeUri: ../lambdas/cfn-cognito-user-pools-domain - Handler: index.handler - Role: !GetAtt CognitoUserPoolDomainBackingFnRole.Arn - CognitoUserPoolDomain: - Type: AWS::CloudFormation::CustomResource + Type: AWS::Cognito::UserPoolDomain Properties: - Timeout: 360 - ServiceToken: !GetAtt CognitoUserPoolDomainBackingFn.Arn UserPoolId: !Ref CognitoUserPool Domain: !Ref CognitoDomainNameOrPrefix - + CognitoIdentityPool: Type: AWS::Cognito::IdentityPool Properties: @@ -1356,7 +1250,7 @@ Resources: IdentityPoolId: !Ref CognitoIdentityPool UserPoolId: !Ref CognitoUserPool UserPoolClientId: !Ref CognitoUserPoolClient - UserPoolDomain: !GetAtt CognitoUserPoolDomain.FullUrl + UserPoolDomain: !Sub "https://${CognitoDomainNameOrPrefix}.auth.ap-southeast-2.amazoncognito.com" MarketplaceSuffix: !Ref MarketplaceSubscriptionTopicProductCode RebuildToken: !Ref StaticAssetRebuildToken RebuildMode: !Ref StaticAssetRebuildMode diff --git a/lambdas/cfn-cognito-user-pools-client-settings/index.js b/lambdas/cfn-cognito-user-pools-client-settings/index.js deleted file mode 100644 index f7661f8bc..000000000 --- a/lambdas/cfn-cognito-user-pools-client-settings/index.js +++ /dev/null @@ -1,53 +0,0 @@ -const AWS = require('aws-sdk'); -const notifyCFN = require('./notify-cfn') - -exports.handler = async (event, context) => { - try { - let responseData - switch (event.RequestType) { - case 'Create': - case 'Update': - var cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider(); - - responseData = await cognitoIdentityServiceProvider.updateUserPoolClient({ - UserPoolId: event.ResourceProperties.UserPoolId, - ClientId: event.ResourceProperties.UserPoolClientId, - SupportedIdentityProviders: event.ResourceProperties.SupportedIdentityProviders, - // make sure these are array-wrapped, but not double-wrapped - CallbackURLs: Array.isArray(event.ResourceProperties.CallbackURL) ? - event.ResourceProperties.CallbackURL : - [event.ResourceProperties.CallbackURL], - LogoutURLs: Array.isArray(event.ResourceProperties.LogoutURL) ? - event.ResourceProperties.LogoutURL : - [event.ResourceProperties.LogoutURL], - AllowedOAuthFlowsUserPoolClient: (event.ResourceProperties.AllowedOAuthFlowsUserPoolClient == 'true'), - AllowedOAuthFlows: event.ResourceProperties.AllowedOAuthFlows, - AllowedOAuthScopes: event.ResourceProperties.AllowedOAuthScopes - }).promise(); - - break; - - case 'Delete': break; // these are just extra settings on a userPoolClient, so we don't actually do a delete action - } - - // trim a useless layer of JSON - if (responseData && responseData.UserPoolClient) - responseData = responseData.UserPoolClient - - // try to use the User Pool id (plus '-Settings') as the ID - let physicalResourceId - - if (responseData && responseData.UserPoolId) - physicalResourceId = responseData.UserPoolId + '-Settings' - - await notifyCFN.ofSuccess({ event, context, responseData, physicalResourceId }) - - console.info(`CognitoUserPoolClientSettings Success for request type ${event.RequestType}`); - - } catch (error) { - - await notifyCFN.ofFailure({ error, event, context }) - - console.error(`CognitoUserPoolClientSettings Error for request type ${event.RequestType}:`, error); - } -} \ No newline at end of file diff --git a/lambdas/cfn-cognito-user-pools-client-settings/notify-cfn.js b/lambdas/cfn-cognito-user-pools-client-settings/notify-cfn.js deleted file mode 100644 index fb9cca7ef..000000000 --- a/lambdas/cfn-cognito-user-pools-client-settings/notify-cfn.js +++ /dev/null @@ -1,108 +0,0 @@ -const crypto = require('crypto') - -let lastRequestId = '' - -/** - * - * The config object used to define the response sent to CloudFormation. - * - * @typedef {Object} ResponseConfig - * - * @property {Object} event - The event object passed in from the lambda function. - * @property {Object} context - The context object passed in from the lambda function. Required if logStream is not present. - * @property {string} responseStatus - Indicates if the response was a SUCCESS or FAILURE. - * @property {Object} responseData - Data to pass to a successful response. Required on SUCCESSes. - * @property {string|Object} error - The error object or message that indicates failure. Required on FAILUREs. - * @property {string} [physicalResourceId] - An optional id. Generated from the responseData if not provided. - * @property {string} [logicalResourceId] - An optional id. Uses the generating lambda function if not provided. - */ - -/** - * Uses the cfn-response library to notify CloudFormation that this custom resource is done with the task it was - * invoked to do. This could be the response to a create / update request (which would upload files from S3) or a - * delete request (which would delete files from S3). - * - * Returns a Promise b/c it's being used inside an async function. - * - * @param {ResponseConfig} config - context lambda function context - */ -function notifyCFN ({ event, context, responseStatus, responseData, error, physicalResourceId, logicalResourceId }) { - // if (lastRequestId === event.RequestId) - // return Promise.reject("Attempted to run `notifyCFN` more than once with the same responseURL. I'm afraid I can't let you do that, Dave.") - - // lastRequestId = event.RequestId - - return new Promise((resolve, reject) => { - let errorMessage = error ? error.message || error.error || error.errorMessage || error : '' - - // if there's no response message, default it based on presence of an error - if (!responseStatus) - responseStatus = error ? 'FAILED' : 'SUCCESS' - - // if there's no physicalResourceId and it's a SUCCESS, fake a physicalResourceId from the responseData - if (!physicalResourceId) - physicalResourceId = crypto.createHash('md5').update(JSON.stringify(responseData) || '{}').digest('hex') - - if (!logicalResourceId) logicalResourceId = event.LogicalResourceId - - var responseBody = JSON.stringify({ - Status: responseStatus, - Reason: (errorMessage || '') + "\n For more details, see CloudWatch group (" + context.logGroupName + ') and stream (' + context.logStreamName + ')', - PhysicalResourceId: physicalResourceId, - StackId: event.StackId, - RequestId: event.RequestId, - LogicalResourceId: logicalResourceId, // the lambda function; do we want this? - Data: responseData - }); - - console.log("Response body:\n", responseBody); - - var https = require("https"); - var url = require("url"); - - var parsedUrl = url.parse(event.ResponseURL); - var options = { - hostname: parsedUrl.hostname, - port: 443, - path: parsedUrl.path, - method: "PUT", - headers: { - "content-type": "", - "content-length": responseBody.length - } - }; - - var request = https.request(options, function (response) { - console.log("Status code: " + response.statusCode); - console.log("Status message: " + response.statusMessage); - resolve(response); - }); - - request.on("error", function (error) { - console.log("send(..) failed executing https.request(..): " + error); - reject(error); - }); - - request.write(responseBody); - request.end(); - }) -} - -/** - * Inform CloudFormation this request was a success. - * - * @param {ResponseConfig} config - The config object used to define the response sent to CloudFormation. - */ -function ofSuccess(config) { return notifyCFN({ ...config, responseStatus: 'SUCCESS'}) } - -/** - * Inform CloudFormation this request was a failure. - * - * @param {ResponseConfig} config - The config object used to define the response sent to CloudFormation. - */ -function ofFailure(config) { return notifyCFN({ ...config, responseStatus: 'FAILED'}) } - -notifyCFN.ofSuccess = ofSuccess -notifyCFN.ofFailure = ofFailure - -module.exports = notifyCFN \ No newline at end of file diff --git a/lambdas/cfn-cognito-user-pools-domain/index.js b/lambdas/cfn-cognito-user-pools-domain/index.js deleted file mode 100644 index 1339e07cb..000000000 --- a/lambdas/cfn-cognito-user-pools-domain/index.js +++ /dev/null @@ -1,89 +0,0 @@ -const AWS = require('aws-sdk'); -const notifyCFN = require('./notify-cfn') - -exports.handler = async (event, context) => { - try { - var cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider(); - let responseData - - switch (event.RequestType) { - case 'Create': - await cognitoIdentityServiceProvider.createUserPoolDomain({ - UserPoolId: event.ResourceProperties.UserPoolId, - Domain: event.ResourceProperties.Domain - }).promise(); - - responseData = await cognitoIdentityServiceProvider.describeUserPoolDomain({ - Domain: event.ResourceProperties.Domain - }).promise() - - break; - - case 'Update': - // The only way to change the -domain- on a domain (as opposed to the cert) - // is to delete and recreate. we're making the assumption that an update is - // for a new domain. When we support custom domains, would be a good idea - // to make this logic a bit smarter. - await deleteUserPoolDomain(cognitoIdentityServiceProvider, event.OldResourceProperties.Domain); - - await cognitoIdentityServiceProvider.createUserPoolDomain({ - UserPoolId: event.ResourceProperties.UserPoolId, - Domain: event.ResourceProperties.Domain - }).promise(); - - responseData = await cognitoIdentityServiceProvider.describeUserPoolDomain({ - Domain: event.ResourceProperties.Domain - }).promise() - - break; - - case 'Delete': - await deleteUserPoolDomain(cognitoIdentityServiceProvider, event.ResourceProperties.Domain); - - break; - } - - // trim a useless layer of json - if (responseData && responseData.DomainDescription) - responseData = responseData.DomainDescription - - // try to use the User Pool is (plus '-Settings') as the ID - let physicalResourceId - - if (responseData && responseData.UserPoolId) { - physicalResourceId = responseData.UserPoolId + '-Domain-' + responseData.Domain - - // generate the url - if (responseData.CustomDomainConfig && responseData.CustomDomainConfig.CertificateArn) { - // is a custom domain - responseData.FullUrl = `https://${responseData.Domain}` - } else { - // isn't a custom domain - responseData.FullUrl = `https://${responseData.Domain}.auth.${responseData.UserPoolId.split('_')[0]}.amazoncognito.com` - } - } - - await notifyCFN.ofSuccess({ event, context, responseData, physicalResourceId }) - - console.info(`CognitoUserPoolDomain Success for request type ${event.RequestType}`); - - } catch (error) { - - await notifyCFN.ofFailure({ event, context, error }) - - console.error(`CognitoUserPoolDomain Error for request type ${event.RequestType}:`, error); - } -} - -async function deleteUserPoolDomain(cognitoIdentityServiceProvider, domain) { - var response = await cognitoIdentityServiceProvider.describeUserPoolDomain({ - Domain: domain - }).promise(); - - if (response.DomainDescription.Domain) { - await cognitoIdentityServiceProvider.deleteUserPoolDomain({ - UserPoolId: response.DomainDescription.UserPoolId, - Domain: domain - }).promise(); - } -} \ No newline at end of file diff --git a/lambdas/cfn-cognito-user-pools-domain/notify-cfn.js b/lambdas/cfn-cognito-user-pools-domain/notify-cfn.js deleted file mode 100644 index fb9cca7ef..000000000 --- a/lambdas/cfn-cognito-user-pools-domain/notify-cfn.js +++ /dev/null @@ -1,108 +0,0 @@ -const crypto = require('crypto') - -let lastRequestId = '' - -/** - * - * The config object used to define the response sent to CloudFormation. - * - * @typedef {Object} ResponseConfig - * - * @property {Object} event - The event object passed in from the lambda function. - * @property {Object} context - The context object passed in from the lambda function. Required if logStream is not present. - * @property {string} responseStatus - Indicates if the response was a SUCCESS or FAILURE. - * @property {Object} responseData - Data to pass to a successful response. Required on SUCCESSes. - * @property {string|Object} error - The error object or message that indicates failure. Required on FAILUREs. - * @property {string} [physicalResourceId] - An optional id. Generated from the responseData if not provided. - * @property {string} [logicalResourceId] - An optional id. Uses the generating lambda function if not provided. - */ - -/** - * Uses the cfn-response library to notify CloudFormation that this custom resource is done with the task it was - * invoked to do. This could be the response to a create / update request (which would upload files from S3) or a - * delete request (which would delete files from S3). - * - * Returns a Promise b/c it's being used inside an async function. - * - * @param {ResponseConfig} config - context lambda function context - */ -function notifyCFN ({ event, context, responseStatus, responseData, error, physicalResourceId, logicalResourceId }) { - // if (lastRequestId === event.RequestId) - // return Promise.reject("Attempted to run `notifyCFN` more than once with the same responseURL. I'm afraid I can't let you do that, Dave.") - - // lastRequestId = event.RequestId - - return new Promise((resolve, reject) => { - let errorMessage = error ? error.message || error.error || error.errorMessage || error : '' - - // if there's no response message, default it based on presence of an error - if (!responseStatus) - responseStatus = error ? 'FAILED' : 'SUCCESS' - - // if there's no physicalResourceId and it's a SUCCESS, fake a physicalResourceId from the responseData - if (!physicalResourceId) - physicalResourceId = crypto.createHash('md5').update(JSON.stringify(responseData) || '{}').digest('hex') - - if (!logicalResourceId) logicalResourceId = event.LogicalResourceId - - var responseBody = JSON.stringify({ - Status: responseStatus, - Reason: (errorMessage || '') + "\n For more details, see CloudWatch group (" + context.logGroupName + ') and stream (' + context.logStreamName + ')', - PhysicalResourceId: physicalResourceId, - StackId: event.StackId, - RequestId: event.RequestId, - LogicalResourceId: logicalResourceId, // the lambda function; do we want this? - Data: responseData - }); - - console.log("Response body:\n", responseBody); - - var https = require("https"); - var url = require("url"); - - var parsedUrl = url.parse(event.ResponseURL); - var options = { - hostname: parsedUrl.hostname, - port: 443, - path: parsedUrl.path, - method: "PUT", - headers: { - "content-type": "", - "content-length": responseBody.length - } - }; - - var request = https.request(options, function (response) { - console.log("Status code: " + response.statusCode); - console.log("Status message: " + response.statusMessage); - resolve(response); - }); - - request.on("error", function (error) { - console.log("send(..) failed executing https.request(..): " + error); - reject(error); - }); - - request.write(responseBody); - request.end(); - }) -} - -/** - * Inform CloudFormation this request was a success. - * - * @param {ResponseConfig} config - The config object used to define the response sent to CloudFormation. - */ -function ofSuccess(config) { return notifyCFN({ ...config, responseStatus: 'SUCCESS'}) } - -/** - * Inform CloudFormation this request was a failure. - * - * @param {ResponseConfig} config - The config object used to define the response sent to CloudFormation. - */ -function ofFailure(config) { return notifyCFN({ ...config, responseStatus: 'FAILED'}) } - -notifyCFN.ofSuccess = ofSuccess -notifyCFN.ofFailure = ofFailure - -module.exports = notifyCFN \ No newline at end of file