Merge pull request #2732 from serverless/limit-permissions

Limited Lambda Permission for Events
This commit is contained in:
Eslam λ Hefnawy 2016-11-21 14:43:37 +07:00 committed by GitHub
commit 2cb4dc4dc5
11 changed files with 116 additions and 47 deletions

View File

@ -70,7 +70,7 @@ We're also using the term `normalizedName` or similar terms in this guide. This
|IAM::Policy | IamPolicyLambdaExecution | IamPolicyLambdaExecution |
|Lambda::Function | {normalizedFunctionName}LambdaFunction | HelloLambdaFunction |
|Logs::LogGroup | {normalizedFunctionName}LogGroup | HelloLogGroup |
|Lambda::Permission | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermissionS3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}</li> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1 </li><li>**S3**: HelloLambdaPermissionS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionSometopic</li> |
|Lambda::Permission | <ul><li>**Schedule**: {normalizedFunctionName}LambdaPermissionEventsRuleSchedule{index} </li><li>**S3**: {normalizedFunctionName}LambdaPermission{normalizedBucketName}S3</li><li>**APIG**: {normalizedFunctionName}LambdaPermissionApiGateway</li><li>**SNS**: {normalizedFunctionName}LambdaPermission{normalizedTopicName}SNS</li> | <ul><li>**Schedule**: HelloLambdaPermissionEventsRuleSchedule1 </li><li>**S3**: HelloLambdaPermissionBucketS3</li><li>**APIG**: HelloLambdaPermissionApiGateway</li><li>**SNS**: HelloLambdaPermissionTopicSNS</li> |
|Events::Rule | {normalizedFuntionName}EventsRuleSchedule{SequentialID} | HelloEventsRuleSchedule1 |
|ApiGateway::RestApi | ApiGatewayRestApi | ApiGatewayRestApi |
|ApiGateway::Resource | ApiGatewayResource{normalizedPath} | ApiGatewayResourceUsers |

View File

@ -7,6 +7,7 @@ module.exports = {
compileMethods() {
this.apiGatewayMethodLogicalIds = [];
this.permissionMapping = [];
this.validated.events.forEach((event) => {
const resourceId = this.getResourceId(event.http.path);
@ -27,14 +28,20 @@ module.exports = {
template.Properties.ApiKeyRequired = true;
}
_.merge(template,
this.getMethodAuthorization(event.http),
this.getMethodIntegration(event.http, event.functionName),
this.getMethodResponses(event.http)
);
const methodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, event.http.method);
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.functionName);
const singlePermissionMapping = { resourceName, lambdaLogicalId, event };
this.permissionMapping.push(singlePermissionMapping);
_.merge(template,
this.getMethodAuthorization(event.http),
this.getMethodIntegration(event.http, lambdaLogicalId, methodLogicalId),
this.getMethodResponses(event.http)
);
this.apiGatewayMethodLogicalIds.push(methodLogicalId);

View File

@ -3,9 +3,7 @@
const _ = require('lodash');
module.exports = {
getMethodIntegration(http, functionName) {
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(functionName);
getMethodIntegration(http, lambdaLogicalId) {
const integration = {
IntegrationHttpMethod: 'POST',
Type: http.integration,

View File

@ -6,27 +6,36 @@ const BbPromise = require('bluebird');
module.exports = {
compilePermissions() {
this.validated.events.forEach((event) => {
this.permissionMapping.forEach((singlePermissionMapping) => {
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaApiGatewayPermissionLogicalId(event.functionName);
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.functionName);
.getLambdaApiGatewayPermissionLogicalId(singlePermissionMapping.event.functionName);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[lambdaPermissionLogicalId]: {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: {
'Fn::GetAtt': [lambdaLogicalId, 'Arn'],
'Fn::GetAtt': [singlePermissionMapping.lambdaLogicalId, 'Arn'],
},
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
SourceArn: { 'Fn::Join': ['',
[
'arn:aws:execute-api:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':',
{ Ref: this.apiGatewayRestApiLogicalId },
'/*/*',
],
] },
},
},
});
if (event.http.authorizer) {
const authorizer = event.http.authorizer;
if (singlePermissionMapping.event.http.authorizer) {
const authorizer = singlePermissionMapping.event.http.authorizer;
const authorizerPermissionLogicalId = this.provider.naming
.getLambdaApiGatewayPermissionLogicalId(authorizer.name);

View File

@ -18,7 +18,7 @@ describe('#awsCompilePermissions()', () => {
awsCompileApigEvents.validated = {};
});
it('should create permission resource when http events are given', () => {
it('should create limited permission resource scope to REST API', () => {
awsCompileApigEvents.validated.events = [
{
functionName: 'First',
@ -26,17 +26,19 @@ describe('#awsCompilePermissions()', () => {
path: 'foo/bar',
method: 'post',
},
}, {
functionName: 'First',
http: {
path: 'foo/bar',
method: 'get',
},
}, {
functionName: 'Second',
http: {
path: 'bar/foo',
method: 'get',
},
];
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.permissionMapping = [
{
lambdaLogicalId: 'FirstLambdaFunction',
resourceName: 'FooBar',
event: {
http: {
path: 'foo/bar',
method: 'post',
},
functionName: 'First',
},
},
];
@ -45,9 +47,22 @@ describe('#awsCompilePermissions()', () => {
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionApiGateway
.Properties.FunctionName['Fn::GetAtt'][0]).to.equal('FirstLambdaFunction');
const deepObj = { 'Fn::Join': ['',
[
'arn:aws:execute-api:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':',
{ Ref: 'ApiGatewayRestApi' },
'/*/*',
],
] };
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.SecondLambdaPermissionApiGateway
.Properties.FunctionName['Fn::GetAtt'][0]).to.equal('SecondLambdaFunction');
.Resources.FirstLambdaPermissionApiGateway
.Properties.SourceArn).to.deep.equal(deepObj);
});
});
@ -65,6 +80,24 @@ describe('#awsCompilePermissions()', () => {
},
},
];
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.permissionMapping = [
{
lambdaLogicalId: 'FirstLambdaFunction',
resourceName: 'FooBar',
event: {
http: {
authorizer: {
name: 'authorizer',
arn: { 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'] },
},
path: 'foo/bar',
method: 'post',
},
functionName: 'First',
},
},
];
return awsCompileApigEvents.compilePermissions().then(() => {
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.AuthorizerLambdaPermissionApiGateway
@ -74,6 +107,7 @@ describe('#awsCompilePermissions()', () => {
it('should not create permission resources when http events are not given', () => {
awsCompileApigEvents.validated.events = [];
awsCompileApigEvents.permissionMapping = [];
return awsCompileApigEvents.compilePermissions().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources

View File

@ -119,7 +119,8 @@ class AwsCompileS3Events {
filter
);
}
s3EnabledFunctions.push(functionName);
const s3EnabledFunction = { functionName, bucketName };
s3EnabledFunctions.push(s3EnabledFunction);
}
});
}
@ -150,9 +151,9 @@ class AwsCompileS3Events {
// iterate over all functions with S3 events
// and give S3 permission to invoke them all
// by adding Lambda::Permission resource for each
s3EnabledFunctions.forEach(functionName => {
s3EnabledFunctions.forEach(s3EnabledFunction => {
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(functionName);
.getLambdaLogicalId(s3EnabledFunction.functionName);
const permissionTemplate = {
Type: 'AWS::Lambda::Permission',
Properties: {
@ -164,10 +165,16 @@ class AwsCompileS3Events {
},
Action: 'lambda:InvokeFunction',
Principal: 's3.amazonaws.com',
SourceArn: { 'Fn::Join': ['',
[
`arn:aws:s3:::${s3EnabledFunction.bucketName}`,
],
] },
},
};
const lambdaPermissionLogicalId = this.provider.naming
.getLambdaS3PermissionLogicalId(functionName);
.getLambdaS3PermissionLogicalId(s3EnabledFunction.functionName,
s3EnabledFunction.bucketName);
const permissionCFResource = {
[lambdaPermissionLogicalId]: permissionTemplate,
};

View File

@ -118,7 +118,10 @@ describe('AwsCompileS3Events', () => {
.Resources.S3BucketFirstfunctionbuckettwo.Type
).to.equal('AWS::S3::Bucket');
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionS3.Type
.Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type
).to.equal('AWS::Lambda::Permission');
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionFirstfunctionbuckettwoS3.Type
).to.equal('AWS::Lambda::Permission');
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.S3BucketFirstfunctionbuckettwo.Properties.NotificationConfiguration
@ -156,7 +159,7 @@ describe('AwsCompileS3Events', () => {
.Resources.S3BucketFirstfunctionbucketone.Properties.NotificationConfiguration
.LambdaConfigurations.length).to.equal(2);
expect(awsCompileS3Events.serverless.service.provider.compiledCloudFormationTemplate
.Resources.FirstLambdaPermissionS3.Type
.Resources.FirstLambdaPermissionFirstfunctionbucketoneS3.Type
).to.equal('AWS::Lambda::Permission');
});

View File

@ -79,7 +79,17 @@ class AwsCompileSNSEvents {
"Properties": {
"FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Action": "lambda:InvokeFunction",
"Principal": "sns.amazonaws.com"
"Principal": "sns.amazonaws.com",
"SourceArn": {
"Fn::Join": ["",
["arn:aws:sns:",
{ "Ref": "AWS::Region"},
":",
{ "Ref": "AWS::AccountId"},
":",
"${topicName}"]
]
}
}
}
`;

View File

@ -62,10 +62,10 @@ describe('AwsCompileSNSEvents', () => {
.provider.compiledCloudFormationTemplate.Resources.SNSTopicTopic2.Type
).to.equal('AWS::SNS::Topic');
expect(awsCompileSNSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1.Type
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type
).to.equal('AWS::Lambda::Permission');
expect(awsCompileSNSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2.Type
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic2SNS.Type
).to.equal('AWS::Lambda::Permission');
});
@ -96,7 +96,7 @@ describe('AwsCompileSNSEvents', () => {
.Properties.Subscription.length
).to.equal(2);
expect(awsCompileSNSEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1.Type
.provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionTopic1SNS.Type
).to.equal('AWS::Lambda::Permission');
});

View File

@ -136,12 +136,13 @@ module.exports = {
},
// Permissions
getLambdaS3PermissionLogicalId(functionName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionS3`;
getLambdaS3PermissionLogicalId(functionName, bucketName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${this
.normalizeBucketName(bucketName)}S3`;
},
getLambdaSnsPermissionLogicalId(functionName, topicName) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermission${
this.normalizeTopicName(topicName)}`;
this.normalizeTopicName(topicName)}SNS`;
},
getLambdaSchedulePermissionLogicalId(functionName, scheduleIndex) {
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionEventsRuleSchedule${

View File

@ -325,16 +325,16 @@ describe('#naming()', () => {
describe('#getLambdaS3PermissionLogicalId()', () => {
it('should normalize the function name and add the standard suffix', () => {
expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName'))
.to.equal('FunctionNameLambdaPermissionS3');
expect(sdk.naming.getLambdaS3PermissionLogicalId('functionName', 'bucket'))
.to.equal('FunctionNameLambdaPermissionBucketS3');
});
});
describe('#getLambdaSnsPermissionLogicalId()', () => {
it('should normalize the function and topic names and add them as prefix and suffix to the ' +
'standard permission center', () => {
expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topicName'))
.to.equal('FunctionNameLambdaPermissionTopicName');
expect(sdk.naming.getLambdaSnsPermissionLogicalId('functionName', 'topic'))
.to.equal('FunctionNameLambdaPermissionTopicSNS');
});
});