Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cognito User Pool Triggers #3657

Merged
merged 16 commits into from Jun 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/providers/aws/README.md
Expand Up @@ -89,6 +89,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
<li><a href="./events/iot.md">IoT</a></li>
<li><a href="./events/cloudwatch-event.md">CloudWatch Event</a></li>
<li><a href="./events/cloudwatch-log.md">CloudWatch Log</a></li>
<li><a href="./events/cognito-user-pool.md">Cognito User Pool</a></li>
</ul>
</div>
</div>
Expand Down
116 changes: 116 additions & 0 deletions docs/providers/aws/events/cognito-user-pool.md
@@ -0,0 +1,116 @@
<!--
title: Serverless Framework - AWS Lambda Events - Cognito User Pool
menuText: Cognito User Pool
menuOrder: 10
description: Setting up AWS Cognito User Pool Triggers with AWS Lambda via the Serverless Framework
layout: Doc
-->

<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/events/cognito-user-pool)
<!-- DOCS-SITE-LINK:END -->

# Cognito User Pool

## Valid Triggers

Serverless supports all Cognito User Pool Triggers as specified [here][aws-triggers-list].

## Simple event definition

This will create a Cognito User Pool with the specified name. You can reference the same pool multiple times.

```yml
functions:
preSignUp:
handler: preSignUp.handler
events:
- cognitoUserPool:
pool: MyUserPool
trigger: PreSignUp
customMessage:
handler: customMessage.handler
events:
- cognitoUserPool:
pool: MyUserPool
trigger: CustomMessage
```

## Multiple pools event definitions

This will create multiple Cognito User Pools with their specified names:

```yml
functions:
preSignUpForPool1:
handler: preSignUp.handler
events:
- cognitoUserPool:
pool: MyUserPool1
trigger: PreSignUp
preSignUpForPool2:
handler: preSignUp.handler
events:
- cognitoUserPool:
pool: MyUserPool2
trigger: PreSignUp
```

You can also deploy the same function for different user pools:

```yml
functions:
preSignUp:
handler: preSignUp.handler
events:
- cognitoUserPool:
pool: MyUserPool1
trigger: PreSignUp
- cognitoUserPool:
pool: MyUserPool2
trigger: PreSignUp
```

## Custom message trigger handlers

For custom messages, you will need to check `event.triggerSource` type inside your handler function:

```js
// customMessage.js
function handler(event, context, callback) {
if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
// ...
}
if (event.triggerSource === 'CustomMessage_ResendCode') {
// ...
}
}
```

## Overriding a generated User Pool

A Cognito User Pool created by an event can be overridden by using the [logical resource name][logical-resource-names] in `Resources`:

```yml
functions:
preSignUp:
handler: preSignUpForPool1.handler
events:
- cognitoUserPool:
pool: MyUserPool
trigger: PreSignUp
postConfirmation:
handler: postConfirmation.handler
events:
- cognitoUserPool:
pool: MyUserPool
trigger: PostConfirmation

resources:
Resources:
CognitoUserPoolMyUserPool:
Type: AWS::Cognito::UserPool
```

[aws-triggers-list]: http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#cognito-user-pools-lambda-trigger-syntax-shared
[logical-resource-names]: ../guide/resources#aws-cloudformation-resource-reference
3 changes: 2 additions & 1 deletion docs/providers/aws/guide/resources.md
Expand Up @@ -70,7 +70,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This
|Lambda::Function | {normalizedFunctionName}LambdaFunction | HelloLambdaFunction |
|Lambda::Version | {normalizedFunctionName}LambdaVersion{sha256} | HelloLambdaVersionr3pgoTvv1xT4E4NiCL6JG02fl6vIyi7OS1aW0FwAI |
|Logs::LogGroup | {normalizedFunctionName}LogGroup | HelloLogGroup |
|Lambda::Permission | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index}</li><li>**CloudWatch Event**: {normalizedFunctionName}LambdaPermissionEventsRuleCloudWatchEvent{index}</li><li>**CloudWatch Log**: {normalizedFunctionName}LambdaPermissionLogsSubscriptionFilterCloudWatchLog{index}</li><li>**IoT**: {normalizedFunctionName}LambdaPermissionIotTopicRule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS</li><li>**Alexa Skill**: {normalizedFunctionName}LambdaPermissionAlexaSkill</li> </ul> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1</li><li>**CloudWatch Event**: HelloLambdaPermissionEventsRuleCloudWatchEvent1</li><li>**CloudWatch Log**: HelloLambdaPermissionLogsSubscriptionFilterCloudWatchLog1</li><li>**IoT**: HelloLambdaPermissionIotTopicRule1 </li><li>**S3**: HelloLambdaPermissionBucketS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionTopicSNS</li><li>**Alexa Skill**: HelloLambdaPermissionAlexaSkill</li> </ul>|
|Lambda::Permission | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index}</li><li>**CloudWatch Event**: {normalizedFunctionName}LambdaPermissionEventsRuleCloudWatchEvent{index}</li><li>**CloudWatch Log**: {normalizedFunctionName}LambdaPermissionLogsSubscriptionFilterCloudWatchLog{index}</li><li>**IoT**: {normalizedFunctionName}LambdaPermissionIotTopicRule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS</li><li>**Alexa Skill**: {normalizedFunctionName}LambdaPermissionAlexaSkill</li><li>**Cognito User Pool Trigger Source**: {normalizedFunctionName}LambdaPermissionCognitoUserPool{normalizedPoolId}TriggerSource{triggerSource}</li> </ul> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1</li><li>**CloudWatch Event**: HelloLambdaPermissionEventsRuleCloudWatchEvent1</li><li>**CloudWatch Log**: HelloLambdaPermissionLogsSubscriptionFilterCloudWatchLog1</li><li>**IoT**: HelloLambdaPermissionIotTopicRule1 </li><li>**S3**: HelloLambdaPermissionBucketS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionTopicSNS</li><li>**Alexa Skill**: HelloLambdaPermissionAlexaSkill</li><li>**Cognito User Pool Trigger Source**: HelloLambdaPermissionCognitoUserPoolMyPoolTriggerSourceCustomMessage</li> </ul>|
|Events::Rule | <ul><li>**Schedule**: {normalizedFuntionName}EventsRuleSchedule{SequentialID}</li><li>**CloudWatch Event**: {normalizedFuntionName}EventsRuleCloudWatchEvent{SequentialID}</li> </ul> | <ul><li>**Schedule**: HelloEventsRuleSchedule1</li><li>**CloudWatch Event**: HelloEventsRuleCloudWatchEvent1</li></ul> |
|AWS::Logs::SubscriptionFilter | {normalizedFuntionName}LogsSubscriptionFilterCloudWatchLog{SequentialID} | HelloLogsSubscriptionFilterCloudWatchLog1 |
|AWS::IoT::TopicRule | {normalizedFuntionName}IotTopicRule{SequentialID} | HelloIotTopicRule1 |
Expand All @@ -83,6 +83,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This
|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic |
|SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic |
|AWS::Lambda::EventSourceMapping | <ul><li>**DynamoDB:** {normalizedFunctionName}EventSourceMappingDynamodb{tableName}</li><li>**Kinesis:** {normalizedFunctionName}EventSourceMappingKinesis{streamName}</li></ul> | <ul><li>**DynamoDB:** HelloLambdaEventSourceMappingDynamodbUsers</li><li>**Kinesis:** HelloLambdaEventSourceMappingKinesisMystream</li></ul> |
|Cognito::UserPool | CognitoUserPool{normalizedPoolId} | CognitoUserPoolPoolId |

## Override AWS CloudFormation Resource

Expand Down
3 changes: 3 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Expand Up @@ -136,6 +136,9 @@ functions:
- cloudwatchLog:
logGroup: '/aws/lambda/hello'
filter: '{$.userIdentity.type = Root}'
- cognitoUserPool:
pool: MyUserPool
trigger: PreSignUp

# The "Resources" your "Functions" use. Raw AWS CloudFormation goes in here.
resources:
Expand Down
1 change: 1 addition & 0 deletions lib/plugins/Plugins.json
Expand Up @@ -34,6 +34,7 @@
"./aws/package/compile/events/iot/index.js",
"./aws/package/compile/events/cloudWatchEvent/index.js",
"./aws/package/compile/events/cloudWatchLog/index.js",
"./aws/package/compile/events/cognitoUserPool/index.js",
"./aws/deployFunction/index.js",
"./aws/deployList/index.js",
"./aws/invokeLocal/index.js"
Expand Down
10 changes: 10 additions & 0 deletions lib/plugins/aws/lib/naming.js
Expand Up @@ -246,6 +246,11 @@ module.exports = {
.getNormalizedFunctionName(functionName)}LogsSubscriptionFilterCloudWatchLog${logsIndex}`;
},

// Cognito User Pool
getCognitoUserPoolLogicalId(poolId) {
return `CognitoUserPool${this.normalizeNameToAlphaNumericOnly(poolId)}`;
},

// Permissions
getLambdaS3PermissionLogicalId(functionName, bucketName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${this
Expand Down Expand Up @@ -278,4 +283,9 @@ module.exports = {
return `${this.getNormalizedFunctionName(functionName)
}LambdaPermissionLogsSubscriptionFilterCloudWatchLog${logsIndex}`;
},
getLambdaCognitoUserPoolPermissionLogicalId(functionName, poolId, triggerSource) {
return `${this
.getNormalizedFunctionName(functionName)}LambdaPermissionCognitoUserPool${
this.normalizeNameToAlphaNumericOnly(poolId)}TriggerSource${triggerSource}`;
},
};
17 changes: 17 additions & 0 deletions lib/plugins/aws/lib/naming.test.js
Expand Up @@ -388,6 +388,13 @@ describe('#naming()', () => {
});
});

describe('#getCognitoUserPoolLogicalId()', () => {
it('should normalize the user pool name and add the standard prefix', () => {
expect(sdk.naming.getCognitoUserPoolLogicalId('us-east-1_v123sDAS1'))
.to.equal('CognitoUserPoolUseast1v123sDAS1');
});
});

describe('#getLambdaS3PermissionLogicalId()', () => {
it('should normalize the function name and add the standard suffix', () => {
expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName', 'bucket'))
Expand Down Expand Up @@ -463,4 +470,14 @@ describe('#naming()', () => {
.to.equal('FunctionNameLambdaPermissionLogsSubscriptionFilterCloudWatchLog0');
});
});

describe('#getLambdaCognitoUserPoolPermissionLogicalId()', () => {
it('should normalize the function name and add the standard suffix', () => {
expect(sdk.naming.getLambdaCognitoUserPoolPermissionLogicalId(
'functionName',
'Pool1',
'CustomMessage'
)).to.equal('FunctionNameLambdaPermissionCognitoUserPoolPool1TriggerSourceCustomMessage');
});
});
});
164 changes: 164 additions & 0 deletions lib/plugins/aws/package/compile/events/cognitoUserPool/index.js
@@ -0,0 +1,164 @@
'use strict';

const _ = require('lodash');

const validTriggerSources = [
'PreSignUp',
'PostConfirmation',
'PreAuthentication',
'PostAuthentication',
'CustomMessage',
'DefineAuthChallenge',
'CreateAuthChallenge',
'VerifyAuthChallengeResponse',
];

class AwsCompileCognitoUserPoolEvents {
constructor(serverless) {
this.serverless = serverless;
this.provider = this.serverless.getProvider('aws');

this.hooks = {
'package:compileEvents': this.compileCognitoUserPoolEvents.bind(this),
};
}

compileCognitoUserPoolEvents() {
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) {
// 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(' '));
}
}
});
}
});

// Generate CloudFormation templates for Cognito User Pool changes
_.forEach(userPools, (poolName) => {
// Create a `LambdaConfig` object for the CloudFormation template
const currentPoolTriggerFunctions = _.filter(cognitoUserPoolTriggerFunctions, {
poolName,
});

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);

const DependsOn = _.map(currentPoolTriggerFunctions, (value) => this
.provider.naming.getLambdaLogicalId(value.functionName));

const userPoolTemplate = {
Type: 'AWS::Cognito::UserPool',
Properties: {
UserPoolName: poolName,
LambdaConfig: lambdaConfig,
},
DependsOn,
};

const userPoolCFResource = {
[userPoolLogicalId]: userPoolTemplate,
};

_.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);
});
}
}

module.exports = AwsCompileCognitoUserPoolEvents;