diff --git a/lib/stackops/apiGateway.js b/lib/stackops/apiGateway.js index 8d7f218..fdbca2f 100644 --- a/lib/stackops/apiGateway.js +++ b/lib/stackops/apiGateway.js @@ -225,10 +225,14 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac const authorizerType = _.get(authorizer, 'Properties.Type'); if (authorizerType === 'TOKEN' || authorizerType === 'REQUEST') { const uriParts = authorizer.Properties.AuthorizerUri['Fn::Join'][1]; - const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt')); + const isExternalRefAuthorizer = _.every(uriParts, part => !_.startsWith(part, 'arn:aws:lambda')); + if (isExternalRefAuthorizer) { + const funcIndex = _.findIndex(uriParts, part => + _.has(part, 'Fn::GetAtt') || _.startsWith(part, 'arn:aws:lambda')); - // Use the SERVERLESS_ALIAS stage variable to determine the called function alias - uriParts.splice(funcIndex + 1, 0, ':${stageVariables.SERVERLESS_ALIAS}'); + // Use the SERVERLESS_ALIAS stage variable to determine the called function alias + uriParts.splice(funcIndex + 1, 0, ':${stageVariables.SERVERLESS_ALIAS}'); + } } authorizer.Properties.Name = `${authorizer.Properties.Name}-${this._alias}`; @@ -265,9 +269,12 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac const functionName = _.replace(name, /LambdaPermissionApiGateway$/, ''); const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName)); const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName)); + const isExternalRef = _.startsWith(permission.Properties.FunctionName, 'arn:aws:lambda'); // Adjust references and alias permissions - permission.Properties.FunctionName = { Ref: aliasName }; + if (!isExternalRef) { + permission.Properties.FunctionName = { Ref: aliasName }; + } if (permission.Properties.SourceArn) { // Authorizers do not set the SourceArn property permission.Properties.SourceArn = { @@ -285,9 +292,13 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac ] }; } - + // Add dependency on function version - permission.DependsOn = [ versionName, aliasName ]; + if (!isExternalRef) { + permission.DependsOn = [ versionName, aliasName ]; + } else { + permission.DependsOn = _.compact([ versionName, aliasName ]); + } delete stageStack.Resources[name]; }); diff --git a/test/data/auth-stack-2.json b/test/data/auth-stack-2.json new file mode 100644 index 0000000..fc70a41 --- /dev/null +++ b/test/data/auth-stack-2.json @@ -0,0 +1,425 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Resources": { + "ServerlessDeploymentBucket": { + "Type": "AWS::S3::Bucket" + }, + "Testfct1LogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/sls-test-project-dev-testfct1" + } + }, + "TestauthLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/sls-test-project-dev-testauth" + } + }, + "IamRoleLambdaExecution": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Policies": [ + { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "dev", + "sls-test-project", + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testauth:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testfct1:*:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/sls-test-project-dev-testauth:*:*" + } + ] + } + ] + } + } + ], + "Path": "/", + "RoleName": { + "Fn::Join": [ + "-", + [ + "sls-test-project", + "dev", + "us-east-1", + "lambdaRole" + ] + ] + } + } + }, + "Testfct1LambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/sls-test-project/dev/1496754891214-2017-06-06T13:14:51.214Z/sls-test-project.zip" + }, + "FunctionName": "sls-test-project-dev-testfct1", + "Handler": "handlers/testfct1/handler.handle", + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Timeout": 15, + "Description": "Echo function echoes alias", + "Environment": { + "Variables": { + "SERVERLESS_PROJECT_NAME": "sls-test-project", + "SERVERLESS_PROJECT": "sls-test-project", + "SERVERLESS_STAGE": "dev", + "SERVERLESS_REGION": "us-east-1", + "TEST_TABLE_NAME": { + "Ref": "TestDynamoDbTable" + } + } + } + }, + "DependsOn": [ + "Testfct1LogGroup", + "IamRoleLambdaExecution" + ] + }, + "Testfct1LambdaVersionrvZ7KY7UgzQ2OKbOvZoG1zLgodltc7toF3qYeORU": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "Testfct1LambdaFunction" + }, + "CodeSha256": "rvZ7KY7UgzQ2OKbOvZoG1zLgodltc7+to/F3q+YeORU=", + "Description": "Echo function echoes alias" + } + }, + "TestauthLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/sls-test-project/dev/1496754891214-2017-06-06T13:14:51.214Z/sls-test-project.zip" + }, + "FunctionName": "sls-test-project-dev-testauth", + "Handler": "handlers/testauth/handler.handle", + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Timeout": 15, + "Description": "A custom authorizer", + "Environment": { + "Variables": { + "SERVERLESS_PROJECT_NAME": "sls-test-project", + "SERVERLESS_PROJECT": "sls-test-project", + "SERVERLESS_STAGE": "dev", + "SERVERLESS_REGION": "us-east-1", + "TEST_TABLE_NAME": { + "Ref": "TestDynamoDbTable" + } + } + } + }, + "DependsOn": [ + "TestauthLogGroup", + "IamRoleLambdaExecution" + ] + }, + "TestauthLambdaVersionrvZ7KY7UgzQ2OKbOvZoG1zLgodltc7toF3qYeORU": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "TestauthLambdaFunction" + }, + "CodeSha256": "rvZ7KY7UgzQ2OKbOvZoG1zLgodltc7+to/F3q+YeORU=", + "Description": "A custom authorizer" + } + }, + "ApiGatewayRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "dev-sls-test-project" + } + }, + "ApiGatewayResourceFunc1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiGatewayRestApi", + "RootResourceId" + ] + }, + "PathPart": "func1", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + } + } + }, + "ApiGatewayMethodFunc1Get": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RequestParameters": {}, + "ResourceId": { + "Ref": "ApiGatewayResourceFunc1" + }, + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Ref": "TestauthApiGatewayAuthorizer" + }, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Testfct1LambdaFunction", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "MethodResponses": [] + }, + "DependsOn": "TestauthApiGatewayAuthorizer" + }, + "TestauthApiGatewayAuthorizer": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "AuthorizerResultTtlInSeconds": 0, + "IdentitySource": "method.request.header.Authorization", + "Name": "testauth", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "AuthorizerUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + "arn:aws:lambda:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":function:custom-auth", + "/invocations" + ] + ] + }, + "Type": "TOKEN" + } + }, + "TestauthApiGatewayRequestAuthorizer": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "AuthorizerResultTtlInSeconds": 0, + "IdentitySource": "method.request.header.Authorization", + "Name": "testauthrequest", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "AuthorizerUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "TestauthLambdaFunction", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "Type": "REQUEST" + } + }, + "CognitoTestApiGatewayAuthorizer": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "AuthorizerResultTtlInSeconds": 0, + "IdentitySource": "method.request.header.Authorization", + "Name": "cognitoauth", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "ProviderARNs": [ + "arn:aws:cognito-idp:us-west-2:xxxxx:userpool/us-west-xxxx" + ], + "Type": "COGNITO_USER_POOLS" + } + }, + "ApiGatewayDeployment1496754891256": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + }, + "DependsOn": [ + "ApiGatewayMethodFunc1Get" + ] + }, + "Testfct1LambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Fn::GetAtt": [ + "Testfct1LambdaFunction", + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "ApiGatewayRestApi" + }, + "/*/*" + ] + ] + } + } + }, + "TestauthLambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:custom-auth", + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com" + } + } + }, + "Outputs": { + "ServerlessDeploymentBucketName": { + "Value": { + "Ref": "ServerlessDeploymentBucket" + } + }, + "Testfct1LambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "Testfct1LambdaVersionrvZ7KY7UgzQ2OKbOvZoG1zLgodltc7toF3qYeORU" + } + }, + "TestauthLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "TestauthLambdaVersionrvZ7KY7UgzQ2OKbOvZoG1zLgodltc7toF3qYeORU" + } + }, + "ServiceEndpoint": { + "Description": "URL of the service endpoint", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "ApiGatewayRestApi" + }, + ".execute-api.us-east-1.amazonaws.com/dev" + ] + ] + } + } + } +} diff --git a/test/stackops/apiGateway.test.js b/test/stackops/apiGateway.test.js index 8efee40..a4eb9f7 100644 --- a/test/stackops/apiGateway.test.js +++ b/test/stackops/apiGateway.test.js @@ -642,6 +642,7 @@ describe('API Gateway', () => { ] ] }; + const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; const cogAuth = _.cloneDeep(template.Resources.CognitoTestApiGatewayAuthorizer); cogAuth.Properties.Name += "-myAlias"; @@ -661,6 +662,44 @@ describe('API Gateway', () => { ])); }); + it('should support externally referenced custom authorizers', () => { + stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); + const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; + const compiledAliasTemplate = serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled + .then(() => BbPromise.all([ + expect(template) + .to.have.a.nested.property("Resources.ApiGatewayMethodFunc1Get.Properties.AuthorizerId") + .that.deep.equals({ Ref: "TestauthApiGatewayAuthorizermyAlias" }), + expect(template) + .to.have.a.nested.property("Resources.ApiGatewayMethodFunc1Get.DependsOn") + .that.equals("TestauthApiGatewayAuthorizermyAlias"), + expect(template) + .to.have.a.nested.property('Resources.TestauthApiGatewayAuthorizermyAlias.Properties.AuthorizerUri') + .that.deep.equals({ + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + "arn:aws:lambda:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":function:custom-auth", + "/invocations" + ] + ]}), + expect(compiledAliasTemplate) + .to.have.a.nested.property('Resources.TestauthLambdaPermissionApiGateway.DependsOn') + .that.is.empty + ])); + + }); + it('should transform string dependencies and references to authorizers', () => { const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate;