Repository: HyperBrain/serverless-aws-alias (js)

Issue:
Issue when using custom authorizers with serverless-pseudo-parameters plugin
Hi, 

I stumbled upon this today: 
when using the [serverless-pseudo-parameters](https://www.npmjs.com/package/serverless-pseudo-parameters) plugin, the custom authorizer support breaks. 

    An error occurred: AuthorizerApiGatewayAuthorizerdev - Invalid Authorizer URI: :${stageVariables.SERVERLESS_ALIAS}arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:811794430790:function:authorizer/invocations. Authorizer URI should be a valid API Gateway ARN that represents a Lambda function invocation..

Reason seems to be that the pseudo parameters plugin replaces the direct arn reference of the lambda `arn:aws:lambda: ....` with a call to Cloudformation `Fn::Sub` function (as intented). This break the detection of the custom authorizer as I implemented it.

    ...
    {
       "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:authorizer"
    },
    ...

I created a Pull Request to fix this.


Pull request:
Fix issue wit custom authorizers when using pseudo parameters plugin
Fixes #112 

Patch:
diff --git a/lib/stackops/apiGateway.js b/lib/stackops/apiGateway.js
index fdbca2f..9ca6e1e 100644
--- a/lib/stackops/apiGateway.js
+++ b/lib/stackops/apiGateway.js
@@ -221,17 +221,16 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
 		});
 
 		// Audjust authorizer Uri and name (stage variables are not allowed in Uris here)
+		const isExternalRefAuthorizerPredicate = part => _.startsWith(part, 'arn:aws:lambda') ||
+			(_.has(part, 'Fn::Sub') && _.startsWith(part['Fn::Sub'], 'arn:aws:lambda'));
 		_.forOwn(authorizers, (authorizer, name) => {
 			const authorizerType = _.get(authorizer, 'Properties.Type');
 			if (authorizerType === 'TOKEN' || authorizerType === 'REQUEST') {
 				const uriParts = authorizer.Properties.AuthorizerUri['Fn::Join'][1];
-				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}');
+				const isExternalRefAuthorizer = _.some(uriParts, isExternalRefAuthorizerPredicate);
+				if (!isExternalRefAuthorizer) {
+					const funcIndex = _.findIndex(uriParts, part => _.startsWith(part, '/invocations'));
+					uriParts.splice(funcIndex , 0, ':${stageVariables.SERVERLESS_ALIAS}');
 				}
 			}
 
@@ -269,7 +268,7 @@ 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'); 
+			const isExternalRef = isExternalRefAuthorizerPredicate(permission.Properties.FunctionName); 
 
 			// Adjust references and alias permissions
 			if (!isExternalRef) {


Test patch:
diff --git a/test/data/auth-stack-2.json b/test/data/auth-stack-2.json
index fc70a41..c140230 100644
--- a/test/data/auth-stack-2.json
+++ b/test/data/auth-stack-2.json
@@ -216,6 +216,21 @@
         }
       }
     },
+    "ApiGatewayResourceFunc2": {
+      "Type": "AWS::ApiGateway::Resource",
+      "Properties": {
+        "ParentId": {
+          "Fn::GetAtt": [
+            "ApiGatewayRestApi",
+            "RootResourceId"
+          ]
+        },
+        "PathPart": "func1",
+        "RestApiId": {
+          "Ref": "ApiGatewayRestApi"
+        }
+      }
+    },
     "ApiGatewayMethodFunc1Get": {
       "Type": "AWS::ApiGateway::Method",
       "Properties": {
@@ -258,6 +273,48 @@
       },
       "DependsOn": "TestauthApiGatewayAuthorizer"
     },
+    "ApiGatewayMethodFunc2Get": {
+      "Type": "AWS::ApiGateway::Method",
+      "Properties": {
+        "HttpMethod": "GET",
+        "RequestParameters": {},
+        "ResourceId": {
+          "Ref": "ApiGatewayResourceFunc2"
+        },
+        "RestApiId": {
+          "Ref": "ApiGatewayRestApi"
+        },
+        "AuthorizationType": "CUSTOM",
+        "AuthorizerId": {
+          "Ref": "PseudoParamCustomAuthApiGatewayAuthorizer"
+        },
+        "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": "PseudoParamCustomAuthApiGatewayAuthorizer"
+    },
     "TestauthApiGatewayAuthorizer": {
       "Type": "AWS::ApiGateway::Authorizer",
       "Properties": {
@@ -288,6 +345,34 @@
         "Type": "TOKEN"
       }
     },
+    "PseudoParamCustomAuthApiGatewayAuthorizer": {
+      "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/",
+              {
+                "Fn::Sub": "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:custom-auth"
+              },
+              "/invocations"
+            ]
+          ]
+        },
+        "Type": "TOKEN"
+      }
+    },
     "TestauthApiGatewayRequestAuthorizer": {
       "Type": "AWS::ApiGateway::Authorizer",
       "Properties": {
diff --git a/test/stackops/apiGateway.test.js b/test/stackops/apiGateway.test.js
index a4eb9f7..67bab5d 100644
--- a/test/stackops/apiGateway.test.js
+++ b/test/stackops/apiGateway.test.js
@@ -699,6 +699,39 @@ describe('API Gateway', () => {
 				]));
 
 			});
+			
+			it('should support externally referenced custom authorizers with Pseudo Parameters', () => {
+				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.ApiGatewayMethodFunc2Get.Properties.AuthorizerId")
+							.that.deep.equals({ Ref: "PseudoParamCustomAuthApiGatewayAuthorizermyAlias" }),
+					expect(template)
+						.to.have.a.nested.property("Resources.ApiGatewayMethodFunc2Get.DependsOn")
+							.that.equals("PseudoParamCustomAuthApiGatewayAuthorizermyAlias"),
+					expect(template)
+						.to.have.a.nested.property('Resources.PseudoParamCustomAuthApiGatewayAuthorizermyAlias.Properties.AuthorizerUri')
+							.that.deep.equals({
+								"Fn::Join": [
+									"",
+									[
+										"arn:aws:apigateway:",
+										{
+											"Ref": "AWS::Region"
+										},
+										":lambda:path/2015-03-31/functions/",
+										{
+											"Fn::Sub": "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:custom-auth"
+										},
+										"/invocations"
+									]
+								]}),
+				]));
+
+			});
 
 			it('should transform string dependencies and references to authorizers', () => {
 				const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate;