Repository: HyperBrain/serverless-aws-alias
Pull request #102 (2018-02-13 23:05:08)

Add support for referenced custom authorizer lambdas
Proposed solution for #101 

After digging a bit I found this bit in apiGateway.js

    const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt'));

which selects the index in the `AuthorizerUri. Fn::Join` array based on finding the `GetAtt , ARN` of a referenced lambda function. This won't work if the function is directly referenced using its ARN. In this case the uriPart of the `AuthorizerUri` will simply be a string with the ARN and `funcIndex` will be set to -1. Hence `${stageVariables.SERVERLESS_ALIAS}` will be added to the head of the array. 

I would suggest to add a simple check for strings, e.g.

    const funcIndex = _.findIndex(uriParts, part =>
          _.has(part, 'Fn::GetAtt') || _.startsWith(part, 'arn:aws:lambda'));

Concerning the missing `FunctionName` I would simple use the original one, in case no other `aliasName` can be found, e.g instead of

    permission.Properties.FunctionName = { Ref: aliasName };

use the original 

    permission.Properties.FunctionName = aliasName ? { Ref: aliasName } : permission.Properties.FunctionName;

Background is, that the function name of the referenced custom authorizer can't generally be extended by the alias, as it lives in a different project and might be deployed with a different alias (or no alias at all)

Concerning the null values of the Permission: in this case, there should be no dependencies as the custom authorizer lambda should already be deployed by another project. 

    permission.DependsOn = _.compact([ versionName, aliasName ]);

But this might lead to some unwanted behavior, as you mentioned in #83, and needs some testing 


Patch:
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];
 		});


Test patch:
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;