Skip to content

Commit

Permalink
Multiple event definitions for existing Cognito User Pools
Browse files Browse the repository at this point in the history
  • Loading branch information
pmuens committed Aug 6, 2019
1 parent 5e0d9c2 commit 5c85094
Show file tree
Hide file tree
Showing 11 changed files with 472 additions and 75 deletions.
2 changes: 2 additions & 0 deletions docs/providers/aws/events/cognito-user-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ functions:

Sometimes you might want to attach Lambda functions to existing Cognito User Pools. In that case you just need to set the `existing` event configuration property to `true`. All the other config parameters can also be used on existing user pools:

**IMPORTANT:** You can only attach 1 existing Cognito User Pool per function.

**NOTE:** Using the `existing` config will add an additional Lambda function and IAM Role to your stack. The Lambda function backs-up the Custom Cognito User Pool Resource which is used to support existing user pools.

```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function handler(event, context) {
}

function create(event, context) {
const { FunctionName, UserPoolName, UserPoolConfig } = event.ResourceProperties;
const { FunctionName, UserPoolName, UserPoolConfigs } = event.ResourceProperties;
const { Region, AccountId } = getEnvironment(context);

const lambdaArn = getLambdaArn(Region, AccountId, FunctionName);
Expand All @@ -32,7 +32,7 @@ function create(event, context) {
updateConfiguration({
lambdaArn,
userPoolName: UserPoolName,
userPoolConfig: UserPoolConfig,
userPoolConfigs: UserPoolConfigs,
region: Region,
})
)
Expand All @@ -41,14 +41,14 @@ function create(event, context) {

function update(event, context) {
const { Region, AccountId } = getEnvironment(context);
const { FunctionName, UserPoolName, UserPoolConfig } = event.ResourceProperties;
const { FunctionName, UserPoolName, UserPoolConfigs } = event.ResourceProperties;

const lambdaArn = getLambdaArn(Region, AccountId, FunctionName);

return updateConfiguration({
lambdaArn,
userPoolName: UserPoolName,
userPoolConfig: UserPoolConfig,
userPoolConfigs: UserPoolConfigs,
region: Region,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function getConfiguration(config) {
}

function updateConfiguration(config) {
const { lambdaArn, userPoolConfig, region } = config;
const { lambdaArn, userPoolConfigs, region } = config;
const cognito = new CognitoIdentityServiceProvider({ region });

return getConfiguration(config).then(res => {
Expand All @@ -54,7 +54,9 @@ function updateConfiguration(config) {
return accum;
}, LambdaConfig);

LambdaConfig[userPoolConfig.Trigger] = lambdaArn;
userPoolConfigs.forEach(poolConfig => {
LambdaConfig[poolConfig.Trigger] = lambdaArn;
});

return cognito.updateUserPool({ UserPoolId, LambdaConfig }).promise();
});
Expand Down
8 changes: 6 additions & 2 deletions lib/plugins/aws/lib/naming.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,12 @@ module.exports = {
)}`
);
},
getCustomResourceCognitoUserPoolResourceLogicalId(functionName, idx) {
return `${this.getNormalizedFunctionName(functionName)}CustomCognitoUserPool${idx}`;
getCustomResourceCognitoUserPoolResourceLogicalId(functionName) {
// NOTE: we have to keep the 1 at the end to ensure backwards compatibility
// previously we've used an index to allow the creation of multiple custom
// Cognito User Pool resources
// we're now using one resource to handle multiple Cognito User Pool event definitions
return `${this.getNormalizedFunctionName(functionName)}CustomCognitoUserPool1`;
},
// Event Bridge
getCustomResourceEventBridgeHandlerFunctionName() {
Expand Down
7 changes: 3 additions & 4 deletions lib/plugins/aws/lib/naming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,10 +780,9 @@ describe('#naming()', () => {
describe('#getCustomResourceCognitoUserPoolResourceLogicalId()', () => {
it('should return the logical id of the Cognito User Pool custom resouce', () => {
const functionName = 'my-function';
const index = 1;
expect(
sdk.naming.getCustomResourceCognitoUserPoolResourceLogicalId(functionName, index)
).to.equal('MyDashfunctionCustomCognitoUserPool1');
expect(sdk.naming.getCustomResourceCognitoUserPoolResourceLogicalId(functionName)).to.equal(
'MyDashfunctionCustomCognitoUserPool1'
);
});
});

Expand Down
102 changes: 70 additions & 32 deletions lib/plugins/aws/package/compile/events/cognitoUserPool/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,56 +108,78 @@ class AwsCompileCognitoUserPoolEvents {
const { service } = this.serverless;
const { provider } = service;
const { compiledCloudFormationTemplate } = provider;
const { Resources } = compiledCloudFormationTemplate;
const iamRoleStatements = [];

// used to keep track of the custom resources created for each Cognito User Pool
const poolResources = {};

service.getAllFunctions().forEach(functionName => {
let numEventsForFunc = 0;
let funcUsesExistingCognitoUserPool = false;
const functionObj = service.getFunction(functionName);
const FunctionName = functionObj.name;

if (functionObj.events) {
functionObj.events.forEach((event, idx) => {
functionObj.events.forEach(event => {
if (event.cognitoUserPool && event.cognitoUserPool.existing) {
idx++;
numEventsForFunc++;
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 customPoolResourceLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolResourceLogicalId(
functionName
);

const customCognitoUserPool = {
[customCognitoUserPoolResourceLogicalId]: {
Type: 'Custom::CognitoUserPool',
Version: 1.0,
DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId],
Properties: {
ServiceToken: {
'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'],
},
FunctionName,
UserPoolName: pool,
UserPoolConfig: {
Trigger: trigger,
// 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,
},
],
},
},
},
};

_.merge(compiledCloudFormationTemplate.Resources, customCognitoUserPool);

iamRoleStatements.push({
Effect: 'Allow',
Resource: '*',
Action: [
'cognito-idp:ListUserPools',
'cognito-idp:DescribeUserPool',
'cognito-idp:UpdateUserPool',
],
});
};

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);
}
});
}
Expand All @@ -171,6 +193,22 @@ class AwsCompileCognitoUserPoolEvents {
}
});

// 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.call(this, 'cognitoUserPool', iamRoleStatements);
}
Expand Down

0 comments on commit 5c85094

Please sign in to comment.