UNPKG

serverless

Version:

Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more

358 lines (313 loc) • 12.9 kB
'use strict'; const _ = require('lodash'); const BbPromise = require('bluebird'); const { addCustomResourceToService } = require('../../../customResources'); const validTriggerSources = [ 'PreSignUp', 'PostConfirmation', 'PreAuthentication', 'PostAuthentication', 'PreTokenGeneration', 'CustomMessage', 'DefineAuthChallenge', 'CreateAuthChallenge', 'VerifyAuthChallengeResponse', 'UserMigration', ]; class AwsCompileCognitoUserPoolEvents { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('aws'); this.hooks = { 'package:compileEvents': () => { return BbPromise.bind(this) .then(this.newCognitoUserPools) .then(this.existingCognitoUserPools); }, 'after:package:finalize': this.mergeWithCustomResources.bind(this), }; this.serverless.configSchemaHandler.defineFunctionEvent('aws', 'cognitoUserPool', { type: 'object', properties: { pool: { type: 'string', maxLength: 128, pattern: '^[\\w\\s+=,.@-]+$' }, trigger: { enum: validTriggerSources }, existing: { type: 'boolean' }, }, required: ['pool', 'trigger'], additionalProperties: false, }); } newCognitoUserPools() { const { service } = this.serverless; service.getAllFunctions().forEach((functionName) => { const functionObj = service.getFunction(functionName); if (functionObj.events) { functionObj.events.forEach((event) => { if (event.cognitoUserPool) { // return immediately if it's an existing Cognito User Pool event since we treat them differently if (event.cognitoUserPool.existing) return null; const result = this.findUserPoolsAndFunctions(); const cognitoUserPoolTriggerFunctions = result.cognitoUserPoolTriggerFunctions; const userPools = result.userPools; // Generate CloudFormation templates for Cognito User Pool changes userPools.forEach((poolName) => { const currentPoolTriggerFunctions = cognitoUserPoolTriggerFunctions.filter( (triggerFn) => triggerFn.poolName === poolName ); const userPoolCFResource = this.generateTemplateForPool( poolName, currentPoolTriggerFunctions ); _.merge( this.serverless.service.provider.compiledCloudFormationTemplate.Resources, userPoolCFResource ); }); // Generate CloudFormation templates for IAM permissions to allow Cognito to trigger Lambda cognitoUserPoolTriggerFunctions.forEach((cognitoUserPoolTriggerFunction) => { const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId( cognitoUserPoolTriggerFunction.poolName ); const lambdaLogicalId = this.provider.naming.getLambdaLogicalId( cognitoUserPoolTriggerFunction.functionName ); const permissionTemplate = { Type: 'AWS::Lambda::Permission', Properties: { FunctionName: { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, Action: 'lambda:InvokeFunction', Principal: 'cognito-idp.amazonaws.com', SourceArn: { 'Fn::GetAtt': [userPoolLogicalId, 'Arn'], }, }, }; const lambdaPermissionLogicalId = this.provider.naming.getLambdaCognitoUserPoolPermissionLogicalId( cognitoUserPoolTriggerFunction.functionName, cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource ); const permissionCFResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; _.merge( this.serverless.service.provider.compiledCloudFormationTemplate.Resources, permissionCFResource ); }); } return null; }); } return null; }); } existingCognitoUserPools() { const { service } = this.serverless; const { provider } = service; const { compiledCloudFormationTemplate } = provider; const { Resources } = compiledCloudFormationTemplate; const iamRoleStatements = []; let usesExistingCognitoUserPool = false; // used to keep track of the custom resources created for each Cognito User Pool const poolResources = {}; service.getAllFunctions().forEach((functionName) => { let numEventsForFunc = 0; let currentPoolName = null; let funcUsesExistingCognitoUserPool = false; const functionObj = service.getFunction(functionName); const FunctionName = functionObj.name; if (functionObj.events) { functionObj.events.forEach((event) => { if (event.cognitoUserPool && event.cognitoUserPool.existing) { numEventsForFunc++; const { pool, trigger } = event.cognitoUserPool; usesExistingCognitoUserPool = funcUsesExistingCognitoUserPool = true; if (!currentPoolName) { currentPoolName = pool; } if (pool !== currentPoolName) { const errorMessage = [ 'Only one Cognito User Pool can be configured per function.', ` In "${FunctionName}" you're attempting to configure "${currentPoolName}" and "${pool}" at the same time.`, ].join(''); throw new this.serverless.classes.Error(errorMessage); } const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolHandlerFunctionLogicalId(); const customPoolResourceLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolResourceLogicalId( functionName ); // store how often the custom Cognito User Pool resource is used if (poolResources[pool]) { poolResources[pool] = _.union(poolResources[pool], [customPoolResourceLogicalId]); } else { Object.assign(poolResources, { [pool]: [customPoolResourceLogicalId], }); } let customCognitoUserPoolResource; if (numEventsForFunc === 1) { customCognitoUserPoolResource = { [customPoolResourceLogicalId]: { Type: 'Custom::CognitoUserPool', Version: 1.0, DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId], Properties: { ServiceToken: { 'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'], }, FunctionName, UserPoolName: pool, UserPoolConfigs: [ { Trigger: trigger, }, ], }, }, }; iamRoleStatements.push({ Effect: 'Allow', Resource: '*', Action: [ 'cognito-idp:ListUserPools', 'cognito-idp:DescribeUserPool', 'cognito-idp:UpdateUserPool', ], }); } else { Resources[customPoolResourceLogicalId].Properties.UserPoolConfigs.push({ Trigger: trigger, }); } _.merge(Resources, customCognitoUserPoolResource); } }); } if (funcUsesExistingCognitoUserPool) { iamRoleStatements.push({ Effect: 'Allow', Resource: { 'Fn::Sub': `arn:\${AWS::Partition}:lambda:*:*:function:${FunctionName}`, }, Action: ['lambda:AddPermission', 'lambda:RemovePermission'], }); } }); if (usesExistingCognitoUserPool) { iamRoleStatements.push({ Effect: 'Allow', Resource: { 'Fn::Sub': 'arn:${AWS::Partition}:iam::*:role/*', }, Action: ['iam:PassRole'], }); } // check if we need to add DependsOn clauses in case more than 1 // custom resources are created for one Cognito User Pool (to avoid race conditions) if (Object.keys(poolResources).length > 0) { Object.keys(poolResources).forEach((pool) => { const resources = poolResources[pool]; if (resources.length > 1) { resources.forEach((currResourceLogicalId, idx) => { if (idx > 0) { const prevResourceLogicalId = resources[idx - 1]; Resources[currResourceLogicalId].DependsOn.push(prevResourceLogicalId); } }); } }); } if (iamRoleStatements.length) { return addCustomResourceToService(this.provider, 'cognitoUserPool', iamRoleStatements); } return null; } findUserPoolsAndFunctions() { const userPools = []; const cognitoUserPoolTriggerFunctions = []; // Iterate through all functions declared in `serverless.yml` this.serverless.service.getAllFunctions().forEach((functionName) => { const functionObj = this.serverless.service.getFunction(functionName); if (functionObj.events) { functionObj.events.forEach((event) => { if (event.cognitoUserPool) { if (event.cognitoUserPool.existing) return; // Save trigger functions so we can use them to generate // IAM permissions later cognitoUserPoolTriggerFunctions.push({ functionName, poolName: event.cognitoUserPool.pool, triggerSource: event.cognitoUserPool.trigger, }); // Save user pools so we can use them to generate // CloudFormation resources later userPools.push(event.cognitoUserPool.pool); } }); } }); return { cognitoUserPoolTriggerFunctions, userPools }; } generateTemplateForPool(poolName, currentPoolTriggerFunctions) { const lambdaConfig = currentPoolTriggerFunctions.reduce((result, value) => { const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(value.functionName); // Return a new object to avoid lint errors return Object.assign({}, result, { [value.triggerSource]: { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'], }, }); }, {}); const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName); // Attach `DependsOn` for any relevant Lambdas const DependsOn = currentPoolTriggerFunctions.map((value) => this.provider.naming.getLambdaLogicalId(value.functionName) ); return { [userPoolLogicalId]: { Type: 'AWS::Cognito::UserPool', Properties: { UserPoolName: poolName, LambdaConfig: lambdaConfig, }, DependsOn, }, }; } mergeWithCustomResources() { const result = this.findUserPoolsAndFunctions(); const cognitoUserPoolTriggerFunctions = result.cognitoUserPoolTriggerFunctions; const userPools = result.userPools; userPools.forEach((poolName) => { const currentPoolTriggerFunctions = cognitoUserPoolTriggerFunctions.filter( (triggerFn) => triggerFn.poolName === poolName ); const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName); // If overrides exist in `Resources`, merge them in if (_.get(this.serverless.service.resources, userPoolLogicalId)) { const customUserPool = this.serverless.service.resources[userPoolLogicalId]; const generatedUserPool = this.generateTemplateForPool( poolName, currentPoolTriggerFunctions )[userPoolLogicalId]; // Merge `DependsOn` clauses const customUserPoolDependsOn = _.get(customUserPool, 'DependsOn', []); const DependsOn = generatedUserPool.DependsOn.concat(customUserPoolDependsOn); // Merge default and custom resources, and `DependsOn` clause const mergedTemplate = Object.assign({}, _.merge(generatedUserPool, customUserPool), { DependsOn, }); // Merge resource back into `Resources` _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [userPoolLogicalId]: mergedTemplate, }); } }); } } module.exports = AwsCompileCognitoUserPoolEvents;