mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
* fix: remove bluebird from zip-service * fix: remove bluebird and set concurrency limits for packaging
485 lines
16 KiB
JavaScript
485 lines
16 KiB
JavaScript
import _ from 'lodash'
|
|
import { addCustomResourceToService } from '../../../custom-resources/index.js'
|
|
import ServerlessError from '../../../../../serverless-error.js'
|
|
import resolveLambdaTarget from '../../../utils/resolve-lambda-target.js'
|
|
|
|
const customSenderSources = ['CustomSMSSender', 'CustomEmailSender']
|
|
const validTriggerSources = [
|
|
'PreSignUp',
|
|
'PostConfirmation',
|
|
'PreAuthentication',
|
|
'PostAuthentication',
|
|
'PreTokenGeneration',
|
|
'CustomMessage',
|
|
'DefineAuthChallenge',
|
|
'CreateAuthChallenge',
|
|
'VerifyAuthChallengeResponse',
|
|
'UserMigration',
|
|
].concat(customSenderSources)
|
|
|
|
const validLambdaVersions = ['V1_0']
|
|
|
|
class AwsCompileCognitoUserPoolEvents {
|
|
constructor(serverless, options) {
|
|
this.serverless = serverless
|
|
this.options = options
|
|
this.provider = this.serverless.getProvider('aws')
|
|
|
|
this.hooks = {
|
|
'package:compileEvents': async () => {
|
|
return Promise.resolve(this)
|
|
.then(() => this.newCognitoUserPools())
|
|
.then(() => this.existingCognitoUserPools())
|
|
},
|
|
'after:package:finalize': async () => this.mergeWithCustomResources(),
|
|
}
|
|
|
|
this.serverless.configSchemaHandler.defineFunctionEvent(
|
|
'aws',
|
|
'cognitoUserPool',
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
pool: {
|
|
type: 'string',
|
|
maxLength: 128,
|
|
pattern: '^[\\w\\s+=,.@-]+$',
|
|
},
|
|
trigger: { enum: validTriggerSources },
|
|
existing: { type: 'boolean' },
|
|
forceDeploy: { type: 'boolean' },
|
|
kmsKeyId: { $ref: '#/definitions/awsKmsArn' },
|
|
},
|
|
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 triggerFunctionObj = service.getFunction(
|
|
cognitoUserPoolTriggerFunction.functionName,
|
|
)
|
|
|
|
const permissionTemplate = {
|
|
Type: 'AWS::Lambda::Permission',
|
|
DependsOn: _.get(triggerFunctionObj.targetAlias, 'logicalId'),
|
|
Properties: {
|
|
FunctionName: resolveLambdaTarget(
|
|
cognitoUserPoolTriggerFunction.functionName,
|
|
triggerFunctionObj,
|
|
),
|
|
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 = {}
|
|
const poolKmsIdMap = new Map()
|
|
|
|
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, forceDeploy, kmsKeyId, lambdaVersion } =
|
|
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 ServerlessError(
|
|
errorMessage,
|
|
'COGNITO_MULTIPLE_USER_POOLS_PER_FUNCTION',
|
|
)
|
|
}
|
|
|
|
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
|
|
const forceDeployProperty = forceDeploy ? Date.now() : undefined
|
|
|
|
let userPoolConfig = {
|
|
Trigger: trigger,
|
|
}
|
|
if (customSenderSources.includes(trigger)) {
|
|
userPoolConfig = {
|
|
...userPoolConfig,
|
|
...{
|
|
LambdaVersion: lambdaVersion || validLambdaVersions[0],
|
|
},
|
|
}
|
|
|
|
this.checkKmsArn(kmsKeyId, poolKmsIdMap, pool)
|
|
userPoolConfig.KMSKeyID = kmsKeyId
|
|
}
|
|
|
|
if (numEventsForFunc === 1) {
|
|
if (customSenderSources.includes(trigger) && kmsKeyId) {
|
|
iamRoleStatements.push({
|
|
Effect: 'Allow',
|
|
Resource: kmsKeyId,
|
|
Action: ['kms:CreateGrant'],
|
|
})
|
|
}
|
|
customCognitoUserPoolResource = {
|
|
[customPoolResourceLogicalId]: {
|
|
Type: 'Custom::CognitoUserPool',
|
|
Version: '1.0',
|
|
DependsOn: [
|
|
eventFunctionLogicalId,
|
|
customResourceFunctionLogicalId,
|
|
],
|
|
Properties: {
|
|
ServiceToken: {
|
|
'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'],
|
|
},
|
|
FunctionName,
|
|
UserPoolName: pool,
|
|
UserPoolConfigs: [userPoolConfig],
|
|
ForceDeploy: forceDeployProperty,
|
|
},
|
|
},
|
|
}
|
|
|
|
iamRoleStatements.push({
|
|
Effect: 'Allow',
|
|
Resource: '*',
|
|
Action: [
|
|
'cognito-idp:ListUserPools',
|
|
'cognito-idp:DescribeUserPool',
|
|
'cognito-idp:UpdateUserPool',
|
|
],
|
|
})
|
|
} else {
|
|
Resources[
|
|
customPoolResourceLogicalId
|
|
].Properties.UserPoolConfigs.push(userPoolConfig)
|
|
}
|
|
|
|
_.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
|
|
}
|
|
|
|
checkKmsArn(kmsKeyId, poolKmsIdMap, currentPoolName) {
|
|
// KMSKeyId is only used (and is required) with Custom Sender Sources
|
|
if (!kmsKeyId) {
|
|
throw new ServerlessError(
|
|
`KMS Key must be set when using a Custom Sender Source Trigger (CustomSMSSender and/or CustomEmailSender). Affected Cognito User Pool: "${currentPoolName}".`,
|
|
'COGNITO_KMS_KEY_NOT_SET',
|
|
)
|
|
}
|
|
|
|
const previousKmsId = poolKmsIdMap.get(currentPoolName)
|
|
if (
|
|
previousKmsId !== undefined &&
|
|
previousKmsId !== kmsKeyId &&
|
|
JSON.stringify(previousKmsId) !== JSON.stringify(kmsKeyId)
|
|
) {
|
|
throw new ServerlessError(
|
|
`Only one KMS Key for can be configured per Cognito User Pool. Affected Cognito User Pool: "${currentPoolName}".`,
|
|
'COGNITO_KMS_KEY_ID_NOT_SAME_FOR_SINGLE_USER_POOL',
|
|
)
|
|
}
|
|
poolKmsIdMap.set(currentPoolName, kmsKeyId)
|
|
}
|
|
|
|
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,
|
|
kmsKeyId: event.cognitoUserPool.kmsKeyId,
|
|
lambdaVersion: event.cognitoUserPool.lambdaVersion,
|
|
})
|
|
|
|
// 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 poolKmsIdMap = new Map()
|
|
const lambdaConfig = currentPoolTriggerFunctions.reduce((result, value) => {
|
|
const functionObj = this.serverless.service.getFunction(
|
|
value.functionName,
|
|
)
|
|
|
|
let triggerObject
|
|
if (customSenderSources.includes(value.triggerSource)) {
|
|
triggerObject = {
|
|
[value.triggerSource]: {
|
|
LambdaArn: resolveLambdaTarget(value.functionName, functionObj),
|
|
LambdaVersion: value.lambdaVersion || validLambdaVersions[0],
|
|
},
|
|
}
|
|
this.checkKmsArn(value.kmsKeyId, poolKmsIdMap, poolName)
|
|
triggerObject.KMSKeyID = value.kmsKeyId
|
|
} else {
|
|
triggerObject = {
|
|
[value.triggerSource]: resolveLambdaTarget(
|
|
value.functionName,
|
|
functionObj,
|
|
),
|
|
}
|
|
}
|
|
|
|
// Return a new object to avoid lint errors
|
|
return Object.assign({}, result, triggerObject)
|
|
}, {})
|
|
|
|
const userPoolLogicalId =
|
|
this.provider.naming.getCognitoUserPoolLogicalId(poolName)
|
|
|
|
// Attach `DependsOn` for any relevant Lambdas
|
|
const DependsOn = currentPoolTriggerFunctions.map((value) => {
|
|
const functionObj = this.serverless.service.getFunction(
|
|
value.functionName,
|
|
)
|
|
return (
|
|
_.get(functionObj.targetAlias, 'logicalId') ||
|
|
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,
|
|
},
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
export default AwsCompileCognitoUserPoolEvents
|