diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js index 921a5ee596e..b5f0b77f1ba 100644 --- a/lib/classes/Utils.js +++ b/lib/classes/Utils.js @@ -16,6 +16,7 @@ const isDockerContainer = require('is-docker'); const version = require('../../package.json').version; const segment = require('../utils/segment'); const configUtils = require('../utils/config'); +const awsArnRegExs = require('../plugins/aws/utils/arnRegularExpressions'); class Utils { constructor(serverless) { @@ -206,9 +207,9 @@ class Utils { // For HTTP events, see what authorizer types are enabled if (event.http && event.http.authorizer) { if ((typeof event.http.authorizer === 'string' - && event.http.authorizer.toUpperCase() === 'AWS_IAM') - || (event.http.authorizer.type - && event.http.authorizer.type.toUpperCase() === 'AWS_IAM')) { + && event.http.authorizer.toUpperCase() === 'AWS_IAM') + || (event.http.authorizer.type + && event.http.authorizer.type.toUpperCase() === 'AWS_IAM')) { hasIAMAuthorizer = true; } // There are three ways a user can specify a Custom authorizer: @@ -217,18 +218,19 @@ class Utils { // 2) By listing the name of a function in the same service for the name property // in the authorizer object. // 3) By listing a function's ARN in the arn property of the authorizer object. + if ((typeof event.http.authorizer === 'string' - && event.http.authorizer.toUpperCase() !== 'AWS_IAM' - && !event.http.authorizer.includes('arn:aws:cognito-idp')) - || event.http.authorizer.name - || (event.http.authorizer.arn - && event.http.authorizer.arn.includes('arn:aws:lambda'))) { + && event.http.authorizer.toUpperCase() !== 'AWS_IAM' + && !awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) + || event.http.authorizer.name + || (event.http.authorizer.arn + && awsArnRegExs.lambdaArnExpr.test(event.http.authorizer.arn))) { hasCustomAuthorizer = true; } if ((typeof event.http.authorizer === 'string' - && event.http.authorizer.includes('arn:aws:cognito-idp')) - || (event.http.authorizer.arn - && event.http.authorizer.arn.includes('arn:aws:cognito-idp'))) { + && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer)) + || (event.http.authorizer.arn + && awsArnRegExs.cognitoIdpArnExpr.test(event.http.authorizer.arn))) { hasCognitoAuthorizer = true; } } diff --git a/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.js b/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.js new file mode 100644 index 00000000000..39392ff1099 --- /dev/null +++ b/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function getS3EndpointForRegion(region) { + const strRegion = region.toLowerCase(); + // look for govcloud - currently s3-us-gov-west-1.amazonaws.com + if (strRegion.match(/us-gov/)) return `s3-${strRegion}.amazonaws.com`; + // look for china - currently s3.cn-north-1.amazonaws.com.cn + if (strRegion.match(/cn-/)) return `s3.${strRegion}.amazonaws.com.cn`; + // default s3 endpoint for other regions + return 's3.amazonaws.com'; +}; diff --git a/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.test.js b/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.test.js new file mode 100644 index 00000000000..e543fb59110 --- /dev/null +++ b/lib/plugins/aws/deploy/lib/getS3EndpointForRegion.test.js @@ -0,0 +1,21 @@ +'use strict'; +const expect = require('chai').expect; +const getS3EndpointForRegion = require('./getS3EndpointForRegion'); + +describe('getS3EndpointForRegion', () => { + it('should return standard endpoint for us-east-1', () => { + const expected = 's3.amazonaws.com'; + const actual = getS3EndpointForRegion('us-east-1'); + expect(actual).to.equal(expected); + }); + it('should return govcloud endpoint for us-gov-west-1', () => { + const expected = 's3-us-gov-west-1.amazonaws.com'; + const actual = getS3EndpointForRegion('us-gov-west-1'); + expect(actual).to.equal(expected); + }); + it('should return china endpoint for cn-north-1', () => { + const expected = 's3.cn-north-1.amazonaws.com.cn'; + const actual = getS3EndpointForRegion('cn-north-1'); + expect(actual).to.equal(expected); + }); +}); diff --git a/lib/plugins/aws/deploy/lib/validateTemplate.js b/lib/plugins/aws/deploy/lib/validateTemplate.js index 85265f685e8..6d5bae73c62 100644 --- a/lib/plugins/aws/deploy/lib/validateTemplate.js +++ b/lib/plugins/aws/deploy/lib/validateTemplate.js @@ -1,14 +1,15 @@ 'use strict'; +const getS3EndpointForRegion = require('./getS3EndpointForRegion'); module.exports = { validateTemplate() { const bucketName = this.bucketName; const artifactDirectoryName = this.serverless.service.package.artifactDirectoryName; const compiledTemplateFileName = 'compiled-cloudformation-template.json'; - + const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion()); this.serverless.cli.log('Validating template...'); const params = { - TemplateURL: `https://s3.amazonaws.com/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`, + TemplateURL: `https://${s3Endpoint}/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`, }; return this.provider.request( diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js index 4158893c02c..3b590b866b6 100644 --- a/lib/plugins/aws/deployFunction/index.js +++ b/lib/plugins/aws/deployFunction/index.js @@ -57,19 +57,19 @@ class AwsDeployFunction { 'getFunction', params ) - .then((result) => { - this.serverless.service.provider.remoteFunctionData = result; - return result; - }) - .catch(() => { - const errorMessage = [ - `The function "${this.options.function}" you want to update is not yet deployed.`, - ' Please run "serverless deploy" to deploy your service.', - ' After that you can redeploy your services functions with the', - ' "serverless deploy function" command.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - }); + .then((result) => { + this.serverless.service.provider.remoteFunctionData = result; + return result; + }) + .catch(() => { + const errorMessage = [ + `The function "${this.options.function}" you want to update is not yet deployed.`, + ' Please run "serverless deploy" to deploy your service.', + ' After that you can redeploy your services functions with the', + ' "serverless deploy function" command.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + }); } normalizeArnRole(role) { @@ -84,8 +84,8 @@ class AwsDeployFunction { const roleProperties = roleResource.Properties; const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`; - return this.provider.getAccountId().then((accountId) => - `arn:aws:iam::${accountId}:role${compiledFullRoleName}` + return this.provider.getAccountInfo().then((result) => + `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}` ); } diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js index ecbf65ddd0f..a1c28f0ae4d 100644 --- a/lib/plugins/aws/deployFunction/index.test.js +++ b/lib/plugins/aws/deployFunction/index.test.js @@ -120,13 +120,13 @@ describe('AwsDeployFunction', () => { }); describe('#normalizeArnRole', () => { - let getAccountIdStub; + let getAccountInfoStub; let getRoleStub; beforeEach(() => { - getAccountIdStub = sinon - .stub(awsDeployFunction.provider, 'getAccountId') - .resolves('123456789012'); + getAccountInfoStub = sinon + .stub(awsDeployFunction.provider, 'getAccountInfo') + .resolves({ accountId: '123456789012', partition: 'aws' }); getRoleStub = sinon .stub(awsDeployFunction.provider, 'request') .resolves({ Arn: 'arn:aws:iam::123456789012:role/role_2' }); @@ -144,7 +144,7 @@ describe('AwsDeployFunction', () => { }); afterEach(() => { - awsDeployFunction.provider.getAccountId.restore(); + awsDeployFunction.provider.getAccountInfo.restore(); awsDeployFunction.provider.request.restore(); serverless.service.resources = undefined; }); @@ -153,7 +153,7 @@ describe('AwsDeployFunction', () => { const arn = 'arn:aws:iam::123456789012:role/role'; return awsDeployFunction.normalizeArnRole(arn).then((result) => { - expect(getAccountIdStub.calledOnce).to.be.equal(false); + expect(getAccountInfoStub.calledOnce).to.be.equal(false); expect(result).to.be.equal(arn); }); }); @@ -162,7 +162,7 @@ describe('AwsDeployFunction', () => { const roleName = 'MyCustomRole'; return awsDeployFunction.normalizeArnRole(roleName).then((result) => { - expect(getAccountIdStub.calledOnce).to.be.equal(true); + expect(getAccountInfoStub.calledOnce).to.be.equal(true); expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_123'); }); }); @@ -177,7 +177,7 @@ describe('AwsDeployFunction', () => { return awsDeployFunction.normalizeArnRole(roleObj).then((result) => { expect(getRoleStub.calledOnce).to.be.equal(true); - expect(getAccountIdStub.calledOnce).to.be.equal(false); + expect(getAccountInfoStub.calledOnce).to.be.equal(false); expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_2'); }); }); @@ -336,7 +336,7 @@ describe('AwsDeployFunction', () => { awsDeployFunction.options = options; return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled - .then(() => expect(updateFunctionConfigurationStub).to.not.be.called); + .then(() => expect(updateFunctionConfigurationStub).to.not.be.called); }); it('should fail when using invalid characters in environment variable', () => { diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js index 5ac87bf5993..71755cc1792 100644 --- a/lib/plugins/aws/lib/updateStack.js +++ b/lib/plugins/aws/lib/updateStack.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); +const getS3EndpointForRegion = require('../deploy/lib/getS3EndpointForRegion'); const NO_UPDATE_MESSAGE = 'No updates are to be performed.'; @@ -13,7 +14,8 @@ module.exports = { const stackName = this.provider.naming.getStackName(); let stackTags = { STAGE: this.provider.getStage() }; const compiledTemplateFileName = 'compiled-cloudformation-template.json'; - const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`; + const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion()); + const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`; // Merge additional stack tags if (typeof this.serverless.service.provider.stackTags === 'object') { @@ -44,7 +46,8 @@ module.exports = { update() { const compiledTemplateFileName = 'compiled-cloudformation-template.json'; - const templateUrl = `https://s3.amazonaws.com/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`; + const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion()); + const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`; this.serverless.cli.log('Updating Stack...'); const stackName = this.provider.naming.getStackName(); @@ -72,7 +75,7 @@ module.exports = { // Policy must have at least one statement, otherwise no updates would be possible at all if (this.serverless.service.provider.stackPolicy && - this.serverless.service.provider.stackPolicy.length) { + this.serverless.service.provider.stackPolicy.length) { params.StackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js index 5d15ad887ef..7f00693dfb6 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.js @@ -2,6 +2,7 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); +const awsArnRegExs = require('../../../../../utils/arnRegularExpressions'); module.exports = { compileAuthorizers() { @@ -23,20 +24,25 @@ module.exports = { const authorizerLogicalId = this.provider.naming.getAuthorizerLogicalId(authorizer.name); - if (typeof authorizer.arn === 'string' && authorizer.arn.match(/^arn:aws:cognito-idp/)) { + if (typeof authorizer.arn === 'string' + && authorizer.arn.match(awsArnRegExs.cognitoIdpArnExpr)) { authorizerProperties.Type = 'COGNITO_USER_POOLS'; authorizerProperties.ProviderARNs = [authorizer.arn]; } else { authorizerProperties.AuthorizerUri = - { 'Fn::Join': ['', - [ - 'arn:aws:apigateway:', - { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', - authorizer.arn, - '/invocations', + { + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + authorizer.arn, + '/invocations', + ], ], - ] }; + }; authorizerProperties.Type = authorizer.type ? authorizer.type.toUpperCase() : 'TOKEN'; } diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js index ee84ad8a24b..5ec1dc89366 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/authorizers.test.js @@ -39,15 +39,18 @@ describe('#compileAuthorizers()', () => { expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); expect(resource.Properties.AuthorizerResultTtlInSeconds).to.equal(300); - expect(resource.Properties.AuthorizerUri).to.deep.equal({ 'Fn::Join': ['', - [ - 'arn:aws:apigateway:', - { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', - { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] }, - '/invocations', + expect(resource.Properties.AuthorizerUri).to.deep.equal({ + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] }, + '/invocations', + ], ], - ], }); expect(resource.Properties.IdentitySource).to.equal('method.request.header.Authorization'); expect(resource.Properties.IdentityValidationExpression).to.equal(undefined); @@ -77,15 +80,18 @@ describe('#compileAuthorizers()', () => { .compiledCloudFormationTemplate.Resources.AuthorizerApiGatewayAuthorizer; expect(resource.Type).to.equal('AWS::ApiGateway::Authorizer'); - expect(resource.Properties.AuthorizerUri).to.deep.equal({ 'Fn::Join': ['', - [ - 'arn:aws:apigateway:', - { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', - 'foo', - '/invocations', + expect(resource.Properties.AuthorizerUri).to.deep.equal({ + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + 'foo', + '/invocations', + ], ], - ], }); expect(resource.Properties.AuthorizerResultTtlInSeconds).to.equal(500); expect(resource.Properties.IdentitySource).to.equal('method.request.header.Custom'); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js index dd1d804a9a0..55c6d287f06 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js @@ -29,9 +29,11 @@ module.exports = { 'https://', this.provider.getApiGatewayRestApiId(), `.execute-api.${ - this.provider.getRegion() - }.amazonaws.com/${ - this.provider.getStage() + this.provider.getRegion() + }.`, + { Ref: 'AWS::URLSuffix' }, + `/${ + this.provider.getStage() }`, ], ], diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js index 3013f1e1144..98be9ee278e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.test.js @@ -63,7 +63,9 @@ describe('#compileDeployment()', () => { [ 'https://', { Ref: awsCompileApigEvents.apiGatewayRestApiLogicalId }, - '.execute-api.us-east-1.amazonaws.com/dev', + '.execute-api.us-east-1.', + { Ref: 'AWS::URLSuffix' }, + '/dev', ], ], }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js index 62e0acfa61d..29f95b8da2e 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js @@ -1,6 +1,7 @@ 'use strict'; const _ = require('lodash'); +const awsArnRegExs = require('../../../../../../utils/arnRegularExpressions'); module.exports = { getMethodAuthorization(http) { @@ -18,7 +19,8 @@ module.exports = { let authorizationType; const authorizerArn = http.authorizer.arn; - if (typeof authorizerArn === 'string' && authorizerArn.match(/^arn:aws:cognito-idp/)) { + if (typeof authorizerArn === 'string' + && authorizerArn.match(awsArnRegExs.cognitoIdpArnExpr)) { authorizationType = 'COGNITO_USER_POOLS'; } else { authorizationType = 'CUSTOM'; diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js index 2f47e5c50e4..5a882d21b8b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js @@ -456,7 +456,7 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties .Integration.RequestTemplates['application/json'] - ).to.not.match(/undefined/); + ).to.not.match(/undefined/); }); }); @@ -479,8 +479,8 @@ describe('#compileMethods()', () => { return awsCompileApigEvents.compileMethods().then(() => { const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties + .Integration.RequestTemplates['application/json']; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); @@ -507,8 +507,8 @@ describe('#compileMethods()', () => { return awsCompileApigEvents.compileMethods().then(() => { const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties + .Integration.RequestTemplates['application/json']; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); @@ -536,8 +536,8 @@ describe('#compileMethods()', () => { return awsCompileApigEvents.compileMethods().then(() => { const jsonRequestTemplatesString = awsCompileApigEvents.serverless.service.provider - .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties - .Integration.RequestTemplates['application/json']; + .compiledCloudFormationTemplate.Resources.ApiGatewayMethodUsersCreatePost.Properties + .Integration.RequestTemplates['application/json']; const cognitoPoolClaimsRegex = /"cognitoPoolClaims"\s*:\s*(\{[^}]*\})/; const cognitoPoolClaimsString = jsonRequestTemplatesString.match(cognitoPoolClaimsRegex)[1]; const cognitoPoolClaims = JSON.parse(cognitoPoolClaimsString); @@ -568,7 +568,7 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties .Integration.RequestTemplates['application/json'] - ).to.not.match(/extraCognitoPoolClaims/); + ).to.not.match(/extraCognitoPoolClaims/); }); }); @@ -665,7 +665,9 @@ describe('#compileMethods()', () => { ).to.deep.equal({ 'Fn::Join': [ '', [ - 'arn:aws:apigateway:', { Ref: 'AWS::Region' }, + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }, '/invocations', ], @@ -677,7 +679,9 @@ describe('#compileMethods()', () => { ).to.deep.equal({ 'Fn::Join': [ '', [ - 'arn:aws:apigateway:', { Ref: 'AWS::Region' }, + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'] }, '/invocations', ], diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js index 1a900bd9d9b..3b0a12f05d3 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js @@ -62,7 +62,9 @@ module.exports = { Uri: { 'Fn::Join': ['', [ - 'arn:aws:apigateway:', + 'arn:', + { Ref: 'AWS::Partition' }, + ':apigateway:', { Ref: 'AWS::Region' }, ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] }, @@ -180,7 +182,6 @@ module.exports = { return !_.isEmpty(integrationRequestTemplates) ? integrationRequestTemplates : undefined; }, - getIntegrationRequestParameters(http) { const parameters = {}; if (http.request && http.request.parameters) { diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js index 2c09c0c7ba4..7cd8aba8107 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); +const awsArnRegExs = require('../../../../../utils/arnRegularExpressions'); module.exports = { @@ -18,29 +19,34 @@ module.exports = { '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' }, - ':', - this.provider.getApiGatewayRestApiId(), - '/*/*', + Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, + SourceArn: { + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: this.apiGatewayRestApiLogicalId }, + '/*/*', + ], ], - ] }, + }, }, }, }); if (singlePermissionMapping.event.http.authorizer && - singlePermissionMapping.event.http.authorizer.arn) { + singlePermissionMapping.event.http.authorizer.arn) { const authorizer = singlePermissionMapping.event.http.authorizer; const authorizerPermissionLogicalId = this.provider.naming .getLambdaApiGatewayPermissionLogicalId(authorizer.name); - if (typeof authorizer.arn === 'string' && authorizer.arn.match(/^arn:aws:cognito-idp/)) { + if (typeof authorizer.arn === 'string' + && authorizer.arn.match(awsArnRegExs.cognitoIdpArnExpr)) { return; } @@ -50,7 +56,7 @@ module.exports = { Properties: { FunctionName: authorizer.arn, Action: 'lambda:InvokeFunction', - Principal: 'apigateway.amazonaws.com', + Principal: { 'Fn::Join': ['', ['apigateway.', { Ref: 'AWS::URLSuffix' }]] }, }, }, }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js index 3602cab8f5f..cc89148cf2a 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/permissions.test.js @@ -48,17 +48,21 @@ describe('#awsCompilePermissions()', () => { .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' }, - '/*/*', + const deepObj = { + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'ApiGatewayRestApi' }, + '/*/*', + ], ], - ] }; + }; expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.FirstLambdaPermissionApiGateway diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js index bbafbb75163..1d5f330cd28 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -1,6 +1,7 @@ 'use strict'; const _ = require('lodash'); +const awsArnRegExs = require('../../../../../utils/arnRegularExpressions'); const NOT_FOUND = -1; const DEFAULT_STATUS_CODES = { @@ -258,7 +259,9 @@ module.exports = { const integration = this.getIntegration(http); if (integration === 'AWS_PROXY' - && typeof arn === 'string' && arn.match(/^arn:aws:cognito-idp/) && authorizer.claims) { + && typeof arn === 'string' + && arn.match(awsArnRegExs.cognitoIdpArnExpr) + && authorizer.claims) { const errorMessage = [ 'Cognito claims can only be filtered when using the lambda integration type', ]; diff --git a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js index d8ef7b2e998..19b9e210c26 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchEvent/index.js @@ -104,7 +104,7 @@ class AwsCompileCloudWatchEventEvents { "FunctionName": { "Fn::GetAtt": ["${ lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": "events.amazonaws.com", + "Principal": { "Fn::Join": ["", ["events.", { "Ref": "AWS::URLSuffix" }]] }, "SourceArn": { "Fn::GetAtt": ["${cloudWatchLogicalId}", "Arn"] } } } diff --git a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js index 697d9736d09..f356d0bb779 100644 --- a/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js +++ b/lib/plugins/aws/package/compile/events/cloudWatchLog/index.js @@ -76,7 +76,7 @@ class AwsCompileCloudWatchLogEvents { .getCloudWatchLogLogicalId(functionName, cloudWatchLogNumberInFunction); const lambdaPermissionLogicalId = this.provider.naming .getLambdaCloudWatchLogPermissionLogicalId(functionName, - cloudWatchLogNumberInFunction); + cloudWatchLogNumberInFunction); // unescape quotes once when the first quote is detected escaped const idxFirstSlash = FilterPattern.indexOf('\\'); @@ -98,33 +98,36 @@ class AwsCompileCloudWatchLogEvents { `; const permissionTemplate = ` - { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { "Fn::GetAtt": ["${ + { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { "Fn::GetAtt": ["${ lambdaLogicalId}", "Arn"] }, - "Action": "lambda:InvokeFunction", - "Principal": { - "Fn::Join": [ "", [ - "logs.", - { "Ref": "AWS::Region" }, - ".amazonaws.com" - ] ] - }, - "SourceArn": { - "Fn::Join": [ "", [ - "arn:aws:logs:", - { "Ref": "AWS::Region" }, - ":", - { "Ref": "AWS::AccountId" }, - ":log-group:", - "${LogGroupName}", - ":*" - ] ] - } + "Action": "lambda:InvokeFunction", + "Principal": { + "Fn::Join": [ "", [ + "logs.", + { "Ref": "AWS::Region" }, + ".", + { "Ref": "AWS::URLSuffix" } + ] ] + }, + "SourceArn": { + "Fn::Join": [ "", [ + "arn:", + { "Ref": "AWS::Partition" }, + ":logs:", + { "Ref": "AWS::Region" }, + ":", + { "Ref": "AWS::AccountId" }, + ":log-group:", + "${LogGroupName}", + ":*" + ] ] } } - `; + } + `; const newCloudWatchLogRuleObject = { [cloudWatchLogLogicalId]: JSON.parse(cloudWatchLogRuleTemplate), diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js index 76f0a4c054c..a85c67a4c40 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js @@ -151,7 +151,7 @@ class AwsCompileCognitoUserPoolEvents { ], }, Action: 'lambda:InvokeFunction', - Principal: 'cognito-idp.amazonaws.com', + Principal: { 'Fn::Join': ['', ['cognito-idp.', { Ref: 'AWS::URLSuffix' }]] }, SourceArn: { 'Fn::GetAtt': [ userPoolLogicalId, @@ -162,7 +162,7 @@ class AwsCompileCognitoUserPoolEvents { }; const lambdaPermissionLogicalId = this.provider.naming .getLambdaCognitoUserPoolPermissionLogicalId(cognitoUserPoolTriggerFunction.functionName, - cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource); + cognitoUserPoolTriggerFunction.poolName, cognitoUserPoolTriggerFunction.triggerSource); const permissionCFResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; diff --git a/lib/plugins/aws/package/compile/events/iot/index.js b/lib/plugins/aws/package/compile/events/iot/index.js index 64633ebc7aa..d8d5ef892e0 100644 --- a/lib/plugins/aws/package/compile/events/iot/index.js +++ b/lib/plugins/aws/package/compile/events/iot/index.js @@ -80,10 +80,12 @@ class AwsCompileIoTEvents { "Properties": { "FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": "iot.amazonaws.com", + "Principal": { "Fn::Join": ["", [ "iot.", { "Ref": "AWS::URLSuffix" } ]] }, "SourceArn": { "Fn::Join": ["", [ - "arn:aws:iot:", + "arn:", + { "Ref": "AWS::Partition" }, + ":iot:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, diff --git a/lib/plugins/aws/package/compile/events/s3/index.js b/lib/plugins/aws/package/compile/events/s3/index.js index 707d84aaeb9..4c2c9dc4a0b 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.js +++ b/lib/plugins/aws/package/compile/events/s3/index.js @@ -147,7 +147,7 @@ class AwsCompileS3Events { _.forEach(dependsOnToCreate, (item) => { const lambdaPermissionLogicalId = this.provider.naming .getLambdaS3PermissionLogicalId(item.functionName, - item.bucketName); + item.bucketName); bucketTemplate.DependsOn.push(lambdaPermissionLogicalId); }); @@ -177,17 +177,21 @@ class AwsCompileS3Events { ], }, Action: 'lambda:InvokeFunction', - Principal: 's3.amazonaws.com', - SourceArn: { 'Fn::Join': ['', - [ - `arn:aws:s3:::${s3EnabledFunction.bucketName}`, + Principal: { 'Fn::Join': ['', ['s3.', { Ref: 'AWS::URLSuffix' }]] }, + SourceArn: { + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + `:s3:::${s3EnabledFunction.bucketName}`, + ], ], - ] }, + }, }, }; const lambdaPermissionLogicalId = this.provider.naming .getLambdaS3PermissionLogicalId(s3EnabledFunction.functionName, - s3EnabledFunction.bucketName); + s3EnabledFunction.bucketName); const permissionCFResource = { [lambdaPermissionLogicalId]: permissionTemplate, }; diff --git a/lib/plugins/aws/package/compile/events/schedule/index.js b/lib/plugins/aws/package/compile/events/schedule/index.js index b115262af2a..5ec91bd4646 100644 --- a/lib/plugins/aws/package/compile/events/schedule/index.js +++ b/lib/plugins/aws/package/compile/events/schedule/index.js @@ -114,7 +114,7 @@ class AwsCompileScheduledEvents { "FunctionName": { "Fn::GetAtt": ["${ lambdaLogicalId}", "Arn"] }, "Action": "lambda:InvokeFunction", - "Principal": "events.amazonaws.com", + "Principal": { "Fn::Join": ["", [ "events.", { "Ref": "AWS::URLSuffix" } ]] }, "SourceArn": { "Fn::GetAtt": ["${scheduleLogicalId}", "Arn"] } } } diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js index 69325ec4064..cde37bd2f63 100644 --- a/lib/plugins/aws/package/compile/events/sns/index.js +++ b/lib/plugins/aws/package/compile/events/sns/index.js @@ -123,7 +123,9 @@ class AwsCompileSNSEvents { topicArn = { 'Fn::Join': ['', [ - 'arn:aws:sns:', + 'arn:', + { Ref: 'AWS::Partition' }, + ':sns:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, @@ -166,7 +168,7 @@ class AwsCompileSNSEvents { Properties: { FunctionName: endpoint, Action: 'lambda:InvokeFunction', - Principal: 'sns.amazonaws.com', + Principal: { 'Fn::Join': ['', ['sns.', { Ref: 'AWS::URLSuffix' }]] }, SourceArn: topicArn, }, }, diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index fae3c200071..98341552c09 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -24,6 +24,7 @@ module.exports = { ); const bucketName = this.serverless.service.provider.deploymentBucket; + const isS3TransferAccelerationSupported = this.provider.isS3TransferAccelerationSupported(); const isS3TransferAccelerationEnabled = this.provider.isS3TransferAccelerationEnabled(); const isS3TransferAccelerationDisabled = this.provider.isS3TransferAccelerationDisabled(); @@ -53,7 +54,7 @@ module.exports = { }); } - if (isS3TransferAccelerationEnabled) { + if (isS3TransferAccelerationEnabled && isS3TransferAccelerationSupported) { // enable acceleration via CloudFormation this.serverless.service.provider.compiledCloudFormationTemplate .Resources.ServerlessDeploymentBucket.Properties = { @@ -64,7 +65,7 @@ module.exports = { // keep track of acceleration status via CloudFormation Output this.serverless.service.provider.compiledCloudFormationTemplate .Outputs.ServerlessDeploymentBucketAccelerated = { Value: true }; - } else if (isS3TransferAccelerationDisabled) { + } else if (isS3TransferAccelerationDisabled && isS3TransferAccelerationSupported) { // explicitly disable acceleration via CloudFormation this.serverless.service.provider.compiledCloudFormationTemplate .Resources.ServerlessDeploymentBucket.Properties = { diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js index e4cb5eba22f..72a2ae361ef 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js @@ -161,6 +161,21 @@ describe('#generateCoreTemplate()', () => { }); }); + it('should exclude AccelerateConfiguration for govcloud region', () => { + sinon.stub(awsPlugin.provider, 'request').resolves(); + sinon.stub(serverless.utils, 'writeFileSync').resolves(); + serverless.config.servicePath = './'; + awsPlugin.provider.options.region = 'us-gov-west-1'; + + return awsPlugin.generateCoreTemplate() + .then(() => { + const template = serverless.service.provider.coreCloudFormationTemplate; + expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ + Type: 'AWS::S3::Bucket', + }); + }); + }); + it('should explode if transfer acceleration is both enabled and disabled', () => { sinon.stub(awsPlugin.provider, 'request').resolves(); sinon.stub(serverless.utils, 'writeFileSync').resolves(); diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js index a17e2dffd8a..3829eeab721 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js @@ -32,7 +32,7 @@ module.exports = { if (_.has(this.serverless.service.provider, 'logRetentionInDays')) { if (_.isInteger(this.serverless.service.provider.logRetentionInDays) && - this.serverless.service.provider.logRetentionInDays > 0) { + this.serverless.service.provider.logRetentionInDays > 0) { newLogGroup[logGroupLogicalId].Properties.RetentionInDays = this.serverless.service.provider.logRetentionInDays; } else { @@ -91,8 +91,10 @@ module.exports = { .PolicyDocument .Statement[0] .Resource - .push({ 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}' + - `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*` }); + .push({ + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*`, + }); this.serverless.service.provider.compiledCloudFormationTemplate .Resources[this.provider.naming.getRoleLogicalId()] @@ -101,8 +103,10 @@ module.exports = { .PolicyDocument .Statement[1] .Resource - .push({ 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}' + - `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*:*` }); + .push({ + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + + `:log-group:${this.provider.naming.getLogGroupName(functionObject.name)}:*:*`, + }); }); if (this.serverless.service.provider.iamRoleStatements) { @@ -134,9 +138,15 @@ module.exports = { this.serverless.service.provider.compiledCloudFormationTemplate .Resources[this.provider.naming.getRoleLogicalId()] .Properties - .ManagedPolicyArns = [ - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', - ]; + .ManagedPolicyArns = [{ + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ], + ], + }]; } return BbPromise.resolve(); @@ -154,7 +164,7 @@ module.exports = { } else { const descriptions = statements.map((statement, i) => { const missing = ['Effect', 'Action', 'Resource'].filter( - prop => statement[prop] === undefined); + prop => statement[prop] === undefined); return missing.length === 0 ? null : `statement ${i} is missing the following properties: ${missing.join(', ')}`; }); diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js index 1407989251f..506a01770d5 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js @@ -95,7 +95,7 @@ describe('#mergeIamTemplates()', () => { ], Resource: [ { - 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + `log-group:/aws/lambda/${qualifiedFunction}:*`, }, ], @@ -107,7 +107,7 @@ describe('#mergeIamTemplates()', () => { ], Resource: [ { - 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + `log-group:/aws/lambda/${qualifiedFunction}:*:*`, }, ], @@ -179,7 +179,7 @@ describe('#mergeIamTemplates()', () => { }]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Effect'); + 'missing the following properties: Effect'); }); it('should throw error if a custom IAM policy statement does not have an Action field', () => { @@ -189,7 +189,7 @@ describe('#mergeIamTemplates()', () => { }]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Action'); + 'missing the following properties: Action'); }); it('should throw error if a custom IAM policy statement does not have a Resource field', () => { @@ -199,7 +199,7 @@ describe('#mergeIamTemplates()', () => { }]; expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Resource'); + 'missing the following properties: Resource'); }); it('should throw an error describing all problematics custom IAM policy statements', () => { @@ -234,44 +234,44 @@ describe('#mergeIamTemplates()', () => { LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), }, } - ); + ); }); }); it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given' - , () => { - awsPackage.serverless.service.provider.logRetentionInDays = 5; - const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); - return awsPackage.mergeIamTemplates().then(() => { - expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[normalizedName] - ).to.deep.equal( - { - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), - RetentionInDays: 5, - }, - } - ); + , () => { + awsPackage.serverless.service.provider.logRetentionInDays = 5; + const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); + return awsPackage.mergeIamTemplates().then(() => { + expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate + .Resources[normalizedName] + ).to.deep.equal( + { + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(functionName), + RetentionInDays: 5, + }, + } + ); + }); }); - }); it('should throw error if RetentionInDays is 0 or not an integer' - , () => { - awsPackage.serverless.service.provider.logRetentionInDays = 0; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = 'string'; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = []; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = {}; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = undefined; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = null; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - }); + , () => { + awsPackage.serverless.service.provider.logRetentionInDays = 0; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = 'string'; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = []; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = {}; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = undefined; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + awsPackage.serverless.service.provider.logRetentionInDays = null; + expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + }); it('should add a CloudWatch LogGroup resource if all functions use custom roles', () => { awsPackage.serverless.service.functions[functionName].role = 'something'; @@ -300,7 +300,7 @@ describe('#mergeIamTemplates()', () => { LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func0.name), }, } - ); + ); expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[normalizedNames[1]] ).to.deep.equal( @@ -310,7 +310,7 @@ describe('#mergeIamTemplates()', () => { LogGroupName: awsPackage.provider.naming.getLogGroupName(f.func1.name), }, } - ); + ); }); }); @@ -326,7 +326,7 @@ describe('#mergeIamTemplates()', () => { .Resource ).to.deep.equal([ { - 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + `log-group:/aws/lambda/${qualifiedFunction}:*`, }, ]); @@ -339,7 +339,7 @@ describe('#mergeIamTemplates()', () => { .Resource ).to.deep.equal([ { - 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + `log-group:/aws/lambda/${qualifiedFunction}:*:*`, }, ]); @@ -367,12 +367,16 @@ describe('#mergeIamTemplates()', () => { .Resource ).to.deep.equal( [ - { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func0:*' }, - { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func1:*' }, + { + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + 'log-group:/aws/lambda/func0:*', + }, + { + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + 'log-group:/aws/lambda/func1:*', + }, ] - ); + ); expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[awsPackage.provider.naming.getRoleLogicalId()] .Properties @@ -382,12 +386,16 @@ describe('#mergeIamTemplates()', () => { .Resource ).to.deep.equal( [ - { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func0:*:*' }, - { 'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:' - + 'log-group:/aws/lambda/func1:*:*' }, + { + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + 'log-group:/aws/lambda/func0:*:*', + }, + { + 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:' + + 'log-group:/aws/lambda/func1:*:*', + }, ] - ); + ); }); }); @@ -409,7 +417,7 @@ describe('#mergeIamTemplates()', () => { .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[awsPackage.provider.naming.getRoleLogicalId()] ).to.exist - ); + ); }); it('should not add the default role if role is defined on a provider level', () => { @@ -449,7 +457,7 @@ describe('#mergeIamTemplates()', () => { .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[awsPackage.provider.naming.getRoleLogicalId()] ).to.not.exist - ); + ); }); describe('ManagedPolicyArns property', () => { @@ -463,8 +471,8 @@ describe('#mergeIamTemplates()', () => { return awsPackage.mergeIamTemplates() .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.not.exist + .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns + ).to.not.exist ); }); @@ -478,9 +486,15 @@ describe('#mergeIamTemplates()', () => { .then(() => { expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.deep.equal([ - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', - ]); + ).to.deep.equal([{ + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ], + ], + }]); }); }); @@ -504,9 +518,15 @@ describe('#mergeIamTemplates()', () => { .then(() => { expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate .Resources[awsPackage.provider.naming.getRoleLogicalId()].Properties.ManagedPolicyArns - ).to.deep.equal([ - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', - ]); + ).to.deep.equal([{ + 'Fn::Join': ['', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ], + ], + }]); }); }); @@ -525,8 +545,8 @@ describe('#mergeIamTemplates()', () => { return awsPackage.mergeIamTemplates() .then(() => expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate - .Resources[awsPackage.provider.naming.getRoleLogicalId()] - ).to.not.exist + .Resources[awsPackage.provider.naming.getRoleLogicalId()] + ).to.not.exist ); }); }); diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 6eae516f637..c0e94301789 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -12,6 +12,7 @@ const https = require('https'); const fs = require('fs'); const objectHash = require('object-hash'); const PromiseQueue = require('promise-queue'); +const getS3EndpointForRegion = require('../deploy/lib/getS3EndpointForRegion'); const constants = { providerName: 'aws', @@ -80,8 +81,8 @@ const impl = { const profileCredentials = new AWS.SharedIniFileCredentials(params); if (!(profileCredentials.accessKeyId - || profileCredentials.sessionToken - || profileCredentials.roleArn)) { + || profileCredentials.sessionToken + || profileCredentials.roleArn)) { throw new Error(`Profile ${profile} does not exist`); } @@ -244,27 +245,27 @@ class AwsProvider { return BbPromise.fromCallback(cb => { req.send(cb); }) - .catch(err => { - let message = err.message; - if (err.message === 'Missing credentials in config') { - const errorMessage = [ - 'AWS provider credentials not found.', - ' Learn how to set up AWS provider credentials', - ` in our docs here: ${chalk.green('http://bit.ly/aws-creds-setup')}.`, - ].join(''); - message = errorMessage; - userStats.track('user_awsCredentialsNotFound'); - } - return BbPromise.reject(new this.serverless.classes.Error(message, err.statusCode)); - }); + .catch(err => { + let message = err.message; + if (err.message === 'Missing credentials in config') { + const errorMessage = [ + 'AWS provider credentials not found.', + ' Learn how to set up AWS provider credentials', + ` in our docs here: ${chalk.green('http://bit.ly/aws-creds-setup')}.`, + ].join(''); + message = errorMessage; + userStats.track('user_awsCredentialsNotFound'); + } + return BbPromise.reject(new this.serverless.classes.Error(message, err.statusCode)); + }); }) - .then(data => { - const result = BbPromise.resolve(data); - if (shouldCache) { - _.set(this.requestCache, `${service}.${method}.${paramsHash}`, result); - } - return result; - })); + .then(data => { + const result = BbPromise.resolve(data); + if (shouldCache) { + _.set(this.requestCache, `${service}.${method}.${paramsHash}`, result); + } + return result; + })); if (shouldCache) { _.set(this.requestCache, `${service}.${method}.${paramsHash}`, request); @@ -309,8 +310,17 @@ class AwsProvider { canUseS3TransferAcceleration(service, method) { // TODO enable more S3 APIs? return service === 'S3' - && ['upload', 'putObject'].indexOf(method) !== -1 - && this.isS3TransferAccelerationEnabled(); + && ['upload', 'putObject'].indexOf(method) !== -1 + && this.isS3TransferAccelerationEnabled(); + } + + // This function will be used to block the addition of transfer acceleration options + // to the cloudformation template for regions where acceleration is not supported (ie, govcloud) + isS3TransferAccelerationSupported() { + // Only enable s3 transfer acceleration for standard regions (non govcloud/china) + // since those regions do not yet support it + const endpoint = getS3EndpointForRegion(this.getRegion()); + return endpoint === 's3.amazonaws.com'; } isS3TransferAccelerationEnabled() { @@ -361,9 +371,19 @@ class AwsProvider { || defaultStage; } - getAccountId() { + getAccountInfo() { return this.request('STS', 'getCallerIdentity', {}) - .then((result) => result.Account); + .then((result) => { + const arn = result.Arn; + const accountId = result.Account; + const partition = arn.split(':')[1]; // ex: arn:aws:iam:acctId:user/xyz + return { + accountId, + partition, + arn: result.Arn, + userId: result.UserId, + }; + }); } /** @@ -383,7 +403,7 @@ class AwsProvider { */ getApiGatewayRestApiRootResourceId() { if (this.serverless.service.provider.apiGateway - && this.serverless.service.provider.apiGateway.restApiRootResourceId) { + && this.serverless.service.provider.apiGateway.restApiRootResourceId) { return this.serverless.service.provider.apiGateway.restApiRootResourceId; } return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] }; @@ -394,7 +414,7 @@ class AwsProvider { */ getApiGatewayPredefinedResources() { if (!this.serverless.service.provider.apiGateway - || !this.serverless.service.provider.apiGateway.restApiResources) { + || !this.serverless.service.provider.apiGateway.restApiResources) { return []; } diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js index 297b298b670..d98522b65be 100644 --- a/lib/plugins/aws/provider/awsProvider.test.js +++ b/lib/plugins/aws/provider/awsProvider.test.js @@ -393,9 +393,9 @@ describe('AwsProvider', () => { {}, { useCache: true } ) - .then(data => { - expect(data.called).to.equal(true); - }); + .then(data => { + expect(data.called).to.equal(true); + }); }); it('should resolve to the same response with mutiple parallel requests', () => { @@ -442,19 +442,19 @@ describe('AwsProvider', () => { } return BbPromise.all(requests) - .then(results => { - expect(_.size(results, numTests)); - _.forEach(results, result => { - expect(result).to.deep.equal(expectedResult); + .then(results => { + expect(_.size(results, numTests)); + _.forEach(results, result => { + expect(result).to.deep.equal(expectedResult); + }); + return BbPromise.join( + expect(sendStub).to.have.been.calledOnce, + expect(requestSpy).to.have.callCount(numTests) + ); + }) + .finally(() => { + requestSpy.restore(); }); - return BbPromise.join( - expect(sendStub).to.have.been.calledOnce, - expect(requestSpy).to.have.callCount(numTests) - ); - }) - .finally(() => { - requestSpy.restore(); - }); }); }); }); @@ -835,9 +835,10 @@ describe('AwsProvider', () => { }); }); - describe('#getAccountId()', () => { - it('should return the AWS account id', () => { + describe('#getAccountInfo()', () => { + it('should return the AWS account id and partition', () => { const accountId = '12345678'; + const partition = 'aws'; const stsGetCallerIdentityStub = sinon .stub(awsProvider, 'request') @@ -848,10 +849,11 @@ describe('AwsProvider', () => { Arn: 'arn:aws:sts::123456789012:assumed-role/ROLE-NAME/VWXYZ', }); - return awsProvider.getAccountId() + return awsProvider.getAccountInfo() .then((result) => { expect(stsGetCallerIdentityStub.calledOnce).to.equal(true); - expect(result).to.equal(accountId); + expect(result.accountId).to.equal(accountId); + expect(result.partition).to.equal(partition); awsProvider.request.restore(); }); }); diff --git a/lib/plugins/aws/utils/arnRegularExpressions.js b/lib/plugins/aws/utils/arnRegularExpressions.js new file mode 100644 index 00000000000..3f4f9467f78 --- /dev/null +++ b/lib/plugins/aws/utils/arnRegularExpressions.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + cognitoIdpArnExpr: /^arn:[a-zA-Z-]*:cognito-idp/, + lambdaArnExpr: /arn:[a-zA-Z-]*:lambda/, +}; diff --git a/lib/plugins/platform/platform.js b/lib/plugins/platform/platform.js index 9710a8afaaf..7409157597a 100644 --- a/lib/plugins/platform/platform.js +++ b/lib/plugins/platform/platform.js @@ -35,10 +35,10 @@ function addReadme(attributes, readmePath) { function fetchEndpoint(provider) { return provider .request( - 'CloudFormation', - 'describeStacks', - { StackName: provider.naming.getStackName() }, - { useCache: true } // Use request cache + 'CloudFormation', + 'describeStacks', + { StackName: provider.naming.getStackName() }, + { useCache: true } // Use request cache ) .then(result => { let endpoint = null; @@ -135,7 +135,7 @@ class Platform { const region = this.provider.getRegion(); - return this.provider.getAccountId().then(accountId => + return this.provider.getAccountInfo().then(res => fetchEndpoint(this.provider).then(endpoint => { const funcs = this.serverless.service.getAllFunctions().map(key => { const arnName = functionInfoUtils.aws.getArnName(key, this.serverless); @@ -145,7 +145,7 @@ class Platform { memory: functionInfoUtils.aws.getMemorySize(key, this.serverless), timeout: functionInfoUtils.aws.getTimeout(key, this.serverless), provider: this.serverless.service.provider.name, - originId: `arn:aws:lambda:${region}:${accountId}:function:${arnName}`, + originId: `arn:${res.partition}:lambda:${region}:${res.accountId}:function:${arnName}`, endpoints: functionInfoUtils.aws.getEndpoints(key, this.serverless, endpoint), }; if (this.serverless.service.functions[key].readme) { diff --git a/lib/plugins/platform/platform.test.js b/lib/plugins/platform/platform.test.js index 41cadaa9dec..c8fb72f33d8 100644 --- a/lib/plugins/platform/platform.test.js +++ b/lib/plugins/platform/platform.test.js @@ -105,7 +105,7 @@ describe('Platform', () => { describe('#publishService()', () => { let getAuthTokenStub; - let getAccountIdStub; + let getAccountInfoStub; let endpointsRequestStub; let publishServiceRequestStub; @@ -114,7 +114,9 @@ describe('Platform', () => { // eslint-disable-next-line max-len 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0OTc4ODMwMzMsImV4cCI6MTUyOTQxOTAzMywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm5pY2tuYW1lIjoiam9obmRvZSJ9.GD6sqQR3qLirnrvLKKrmOc7vgsHpqZ3TPwyG8ZI69ig' ); - getAccountIdStub = sinon.stub(platform.provider, 'getAccountId').resolves('acountId123'); + getAccountInfoStub = sinon + .stub(platform.provider, 'getAccountInfo') + .resolves({ accountId: 'acountId123', partition: 'aws' }); endpointsRequestStub = sinon.stub(platform.provider, 'request').resolves({ Stacks: [ { @@ -128,7 +130,7 @@ describe('Platform', () => { afterEach(() => { platform.getAuthToken.restore(); - platform.provider.getAccountId.restore(); + platform.provider.getAccountInfo.restore(); platform.provider.request.restore(); platform.publishServiceRequest.restore(); console.log.restore(); @@ -143,7 +145,7 @@ describe('Platform', () => { return platform.publishService().then(() => { expect(getAuthTokenStub.calledOnce).to.be.equal(true); - expect(getAccountIdStub.calledOnce).to.be.equal(true); + expect(getAccountInfoStub.calledOnce).to.be.equal(true); expect(endpointsRequestStub.calledOnce).to.be.equal(true); expect(publishServiceRequestStub.calledOnce).to.be.equal(true); const expected = { name: 'new-service-2', stage: undefined, functions: [] }; @@ -180,7 +182,7 @@ describe('Platform', () => { return platform.publishService().then(() => { expect(getAuthTokenStub.calledOnce).to.be.equal(true); - expect(getAccountIdStub.calledOnce).to.be.equal(true); + expect(getAccountInfoStub.calledOnce).to.be.equal(true); expect(endpointsRequestStub.calledOnce).to.be.equal(true); expect(publishServiceRequestStub.calledOnce).to.be.equal(true); const expected = { @@ -226,7 +228,7 @@ describe('Platform', () => { return platform.publishService().then(() => { expect(getAuthTokenStub.calledOnce).to.be.equal(true); - expect(getAccountIdStub.calledOnce).to.be.equal(false); + expect(getAccountInfoStub.calledOnce).to.be.equal(false); expect(endpointsRequestStub.calledOnce).to.be.equal(false); expect(publishServiceRequestStub.calledOnce).to.be.equal(false); });