From 5d83a33923d6a30d5e5ec2c4bb34c5f93fefc629 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 24 Apr 2020 15:40:08 +0100 Subject: [PATCH] fix(apigateway): authorizer is not attached to RestApi across projects When an Authorizer is defined in one node project, imported into another where it is wired up with a RestApi, synthesis throws the error 'Authorizer must be attached to a RestApi'. The root cause of this error is the `instanceof` check. If a user has their project set up in a way that more than one instance of the `@aws-cdk/aws-apigateway` module is present in their `node_modules/` folder, even if they are of the exact same version, type checking is bound to fail. Switching instead to a symbol property based comparison, so that type information survives across installations of the module. fixes #7377 --- .../@aws-cdk/aws-apigateway/lib/authorizer.ts | 17 +++++- .../@aws-cdk/aws-apigateway/lib/method.ts | 44 +++++++++++++-- .../test/authorizers/test.lambda.ts | 35 +++++++++++- .../aws-apigateway/test/test.authorizer.ts | 55 +++++++++++++++++++ 4 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts index 3a2f3698a554d..25c06465b23f4 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts @@ -1,14 +1,29 @@ -import { Resource } from '@aws-cdk/core'; +import { Construct, Resource } from '@aws-cdk/core'; import { AuthorizationType } from './method'; import { RestApi } from './restapi'; +const AUTHORIZER_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.Authorizer'); + /** * Base class for all custom authorizers */ export abstract class Authorizer extends Resource implements IAuthorizer { + /** + * Return whether the given object is an Authorizer. + */ + public static isAuthorizer(x: any): x is Authorizer { + return x !== null && typeof(x) === 'object' && AUTHORIZER_SYMBOL in x; + } + public readonly abstract authorizerId: string; public readonly authorizationType?: AuthorizationType = AuthorizationType.CUSTOM; + constructor(scope: Construct, id: string) { + super(scope, id); + + Object.defineProperty(this, AUTHORIZER_SYMBOL, { value: true }); + } + /** * Called when the authorizer is used from a specific REST API. * @internal diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 87f3b047bc5d4..baafd1be53242 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -52,7 +52,7 @@ export interface MethodOptions { * for the integration response to be correctly mapped to a response to the client. * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-settings-method-response.html */ - readonly methodResponses?: MethodResponse[] + readonly methodResponses?: MethodResponse[]; /** * The request parameters that API Gateway accepts. Specify request parameters @@ -65,15 +65,45 @@ export interface MethodOptions { readonly requestParameters?: { [param: string]: boolean }; /** - * The resources that are used for the response's content type. Specify request - * models as key-value pairs (string-to-string mapping), with a content type - * as the key and a Model resource name as the value + * The models which describe data structure of request payload. When + * combined with `requestValidator` or `requestValidatorOptions`, the service + * will validate the API request payload before it reaches the API's Integration (including proxies). + * Specify `requestModels` as key-value pairs, with a content type + * (e.g. `'application/json'`) as the key and an API Gateway Model as the value. + * + * @example + * + * const userModel: apigateway.Model = api.addModel('UserModel', { + * schema: { + * type: apigateway.JsonSchemaType.OBJECT + * properties: { + * userId: { + * type: apigateway.JsonSchema.STRING + * }, + * name: { + * type: apigateway.JsonSchema.STRING + * } + * }, + * required: ['userId'] + * } + * }); + * api.root.addResource('user').addMethod('POST', + * new apigateway.LambdaIntegration(userLambda), { + * requestModels: { + * 'application/json': userModel + * } + * } + * ); + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-settings-method-request.html#setup-method-request-model */ readonly requestModels?: { [param: string]: IModel }; /** * The ID of the associated request validator. * Only one of `requestValidator` or `requestValidatorOptions` must be specified. + * Works together with `requestModels` or `requestParameters` to validate + * the request before it reaches integration like Lambda Proxy Integration. * @default - No default validator */ readonly requestValidator?: IRequestValidator; @@ -84,11 +114,13 @@ export interface MethodOptions { * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-authorizationscopes * @default - no authorization scopes */ - readonly authorizationScopes?: string[] + readonly authorizationScopes?: string[]; /** * Request validator options to create new validator * Only one of `requestValidator` or `requestValidatorOptions` must be specified. + * Works together with `requestModels` or `requestParameters` to validate + * the request before it reaches integration like Lambda Proxy Integration. * @default - No default validator */ readonly requestValidatorOptions?: RequestValidatorOptions; @@ -153,7 +185,7 @@ export class Method extends Resource { `which is different from what is required by the authorizer [${authorizer.authorizationType}]`); } - if (authorizer instanceof Authorizer) { + if (Authorizer.isAuthorizer(authorizer)) { authorizer._attachToApi(this.restApi); } diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts index cf36daa674214..8f9cfe2eab909 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -3,7 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { AuthorizationType, IdentitySource, RequestAuthorizer, RestApi, TokenAuthorizer } from '../../lib'; +import { AuthorizationType, Authorizer, IdentitySource, RequestAuthorizer, RestApi, TokenAuthorizer } from '../../lib'; export = { 'default token authorizer'(test: Test) { @@ -301,4 +301,37 @@ export = { test.done(); }, + + 'token authorizer is of type Authorizer'(test: Test) { + const stack = new Stack(); + + const handler = new lambda.Function(stack, 'token', { + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + const authorizer = new TokenAuthorizer(stack, 'authorizer', { handler }); + + test.ok(Authorizer.isAuthorizer(authorizer), 'TokenAuthorizer is not of type Authorizer'); + + test.done(); + }, + + 'request authorizer is of type Authorizer'(test: Test) { + const stack = new Stack(); + + const handler = new lambda.Function(stack, 'token', { + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + const authorizer = new RequestAuthorizer(stack, 'authorizer', { + handler, + identitySources: [ IdentitySource.header('my-header') ], + }); + + test.ok(Authorizer.isAuthorizer(authorizer), 'RequestAuthorizer is not of type Authorizer'); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts new file mode 100644 index 0000000000000..5791aa616f253 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts @@ -0,0 +1,55 @@ +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { Authorizer, IdentitySource, RequestAuthorizer, RestApi, TokenAuthorizer } from '../lib'; + +export = { + 'isAuthorizer correctly detects an instance of type Authorizer'(test: Test) { + class MyAuthorizer extends Authorizer { + public readonly authorizerId = 'test-authorizer-id'; + public _attachToApi(_: RestApi): void { + // do nothing + } + } + const stack = new Stack(); + const authorizer = new MyAuthorizer(stack, 'authorizer'); + + test.ok(Authorizer.isAuthorizer(authorizer), 'type Authorizer expected but is not'); + test.ok(!Authorizer.isAuthorizer(stack), 'type Authorizer found, when not expected'); + + test.done(); + }, + + 'token authorizer is of type Authorizer'(test: Test) { + const stack = new Stack(); + + const handler = new Function(stack, 'token', { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + const authorizer = new TokenAuthorizer(stack, 'authorizer', { handler }); + + test.ok(Authorizer.isAuthorizer(authorizer), 'TokenAuthorizer is not of type Authorizer'); + + test.done(); + }, + + 'request authorizer is of type Authorizer'(test: Test) { + const stack = new Stack(); + + const handler = new Function(stack, 'token', { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + const authorizer = new RequestAuthorizer(stack, 'authorizer', { + handler, + identitySources: [ IdentitySource.header('my-header') ], + }); + + test.ok(Authorizer.isAuthorizer(authorizer), 'RequestAuthorizer is not of type Authorizer'); + + test.done(); + }, +}; \ No newline at end of file