serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
315 lines (275 loc) • 11.6 kB
JavaScript
'use strict';
const _ = require('lodash');
const BbPromise = require('bluebird');
const { addCustomResourceToService } = require('../../../../customResources');
const validTriggerSources = [
'PreSignUp',
'PostConfirmation',
'PreAuthentication',
'PostAuthentication',
'CustomMessage',
'DefineAuthChallenge',
'CreateAuthChallenge',
'VerifyAuthChallengeResponse',
];
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),
};
}
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
_.forEach(userPools, poolName => {
const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, {
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
_.forEach(cognitoUserPoolTriggerFunctions, 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 iamRoleStatements = [];
service.getAllFunctions().forEach(functionName => {
let funcUsesExistingCognitoUserPool = false;
const functionObj = service.getFunction(functionName);
const FunctionName = functionObj.name;
if (functionObj.events) {
functionObj.events.forEach((event, idx) => {
if (event.cognitoUserPool && event.cognitoUserPool.existing) {
idx++;
const { pool, trigger } = event.cognitoUserPool;
funcUsesExistingCognitoUserPool = true;
const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolHandlerFunctionLogicalId();
const customCognitoUserPoolResourceLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolResourceLogicalId(
functionName,
idx
);
const customCognitoUserPool = {
[customCognitoUserPoolResourceLogicalId]: {
Type: 'Custom::CognitoUserPool',
Version: 1.0,
DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId],
Properties: {
ServiceToken: {
'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'],
},
FunctionName,
UserPoolName: pool,
UserPoolConfig: {
Trigger: trigger,
},
},
},
};
_.merge(compiledCloudFormationTemplate.Resources, customCognitoUserPool);
iamRoleStatements.push({
Effect: 'Allow',
Resource: '*',
Action: [
'cognito-idp:ListUserPools',
'cognito-idp:DescribeUserPool',
'cognito-idp:UpdateUserPool',
],
});
}
});
}
if (funcUsesExistingCognitoUserPool) {
iamRoleStatements.push({
Effect: 'Allow',
Resource: `arn:aws:lambda:*:*:function:${FunctionName}`,
Action: ['lambda:AddPermission', 'lambda:RemovePermission'],
});
}
});
if (iamRoleStatements.length) {
return addCustomResourceToService.call(this, 'cognitoUserPool', iamRoleStatements);
}
return null;
}
findUserPoolsAndFunctions() {
const userPools = [];
const cognitoUserPoolTriggerFunctions = [];
// Iterate through all functions declared in `serverless.yml`
_.forEach(this.serverless.service.getAllFunctions(), functionName => {
const functionObj = this.serverless.service.getFunction(functionName);
if (functionObj.events) {
_.forEach(functionObj.events, event => {
if (event.cognitoUserPool) {
// Check event definition for `cognitoUserPool` object
if (typeof event.cognitoUserPool === 'object') {
// Check `cognitoUserPool` object has required properties
if (!event.cognitoUserPool.pool || !event.cognitoUserPool.trigger) {
throw new this.serverless.classes.Error(
[
`Cognito User Pool event of function "${functionName}" is not an object.`,
'The correct syntax is an object with the "pool" and "trigger" properties.',
'Please check the docs for more info.',
].join(' ')
);
}
// Check `cognitoUserPool` trigger is valid
if (!_.includes(validTriggerSources, event.cognitoUserPool.trigger)) {
throw new this.serverless.classes.Error(
[
'Cognito User Pool trigger source is invalid, must be one of:',
`${validTriggerSources.join(', ')}.`,
'Please check the docs for more info.',
].join(' ')
);
}
// 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);
} else {
throw new this.serverless.classes.Error(
[
`Cognito User Pool event of function "${functionName}" is not an object.`,
'The correct syntax is an object with the "pool" and "trigger" properties.',
'Please check the docs for more info.',
].join(' ')
);
}
}
});
}
});
return { cognitoUserPoolTriggerFunctions, userPools };
}
generateTemplateForPool(poolName, currentPoolTriggerFunctions) {
const lambdaConfig = _.reduce(
currentPoolTriggerFunctions,
(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 = _.map(currentPoolTriggerFunctions, 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;
_.forEach(userPools, poolName => {
const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, { poolName });
const userPoolLogicalId = this.provider.naming.getCognitoUserPoolLogicalId(poolName);
// If overrides exist in `Resources`, merge them in
if (_.has(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;