From a0723b629233adb32ad77f1e0b2c368b5310ffc6 Mon Sep 17 00:00:00 2001 From: Kyle Robertson Date: Thu, 23 Sep 2021 22:34:36 -0400 Subject: [PATCH] feat(aws-apigatewayv2): add support for api-key in websocket apis --- .../aws-apigatewayv2/lib/websocket/api.ts | 24 +++++++++++- .../aws-apigatewayv2/lib/websocket/route.ts | 7 ++++ .../test/websocket/api.test.ts | 22 +++++++++++ .../websocket/integ.api-apikey.expected.json | 13 +++++++ .../test/websocket/integ.api-apikey.ts | 14 +++++++ .../websocket/integ.api-default.expected.json | 12 ++++++ .../test/websocket/integ.api-default.ts | 11 ++++++ .../test/websocket/route.test.ts | 38 +++++++++++++++++++ 8 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index f2f2653c94ee6..de28c3be85067 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -16,16 +16,37 @@ export interface IWebSocketApi extends IApi { _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration } +/** + * Represents the currently available API Key Selection Expressions + */ +export enum ApiKeySelectionExpression { + /** + * x-api-key type + */ + X_API_KEY = '$request.header.x-api-key', + + /** + * usageIdentifierKey type + */ + USAGE_IDENTIFIER_KEY = '$context.authorizer.usageIdentifierKey' +} + /** * Props for WebSocket API */ export interface WebSocketApiProps { /** - * Name for the WebSocket API resoruce + * Name for the WebSocket API resource * @default - id of the WebSocketApi construct. */ readonly apiName?: string; + /** + * An API key selection expression. Currently only supports '$request.header.x-api-key' and '$context.authorizer.usageIdentifierKey' + * @default - none + */ + readonly apiKeySelectionExpression?: ApiKeySelectionExpression + /** * The description of the API. * @default - none @@ -80,6 +101,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { const resource = new CfnApi(this, 'Resource', { name: this.webSocketApiName, + apiKeySelectionExpression: props?.apiKeySelectionExpression, protocolType: 'WEBSOCKET', description: props?.description, routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts index 0588889a603bc..ebaf874963268 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -45,6 +45,12 @@ export interface WebSocketRouteProps extends WebSocketRouteOptions { * The key to this route. */ readonly routeKey: string; + + /** + * Whether the route requires an API Key to be provided + * @default - false + */ + readonly apiKeyRequired?: boolean; } /** @@ -76,6 +82,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { const route = new CfnRoute(this, 'Resource', { apiId: props.webSocketApi.apiId, + apiKeyRequired: props.apiKeyRequired, routeKey: props.routeKey, target: `integrations/${integration.integrationId}`, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index 959555a5c2b7a..10dcda57ec949 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -1,6 +1,7 @@ import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { + ApiKeySelectionExpression, IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, } from '../../lib'; @@ -24,6 +25,27 @@ describe('WebSocketApi', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); }); + test('apiKeySelectionExpression: given a value', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api', { + apiKeySelectionExpression: ApiKeySelectionExpression.USAGE_IDENTIFIER_KEY, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Api', { + ApiKeySelectionExpression: '$context.authorizer.usageIdentifierKey', + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Stage', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Route', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); + }); + test('addRoute: adds a route with passed key', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json new file mode 100644 index 0000000000000..bc0b6f740acc8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "MyWebsocketApiEBAC53DF": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "ApiKeySelectionExpression": "$request.header.x-api-key", + "Name": "MyWebsocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts new file mode 100644 index 0000000000000..fe6205fb6c79c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; +import { ApiKeySelectionExpression } from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets'); + +new apigw.WebSocketApi(stack, 'MyWebsocketApi', { + apiKeySelectionExpression: ApiKeySelectionExpression.X_API_KEY, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json new file mode 100644 index 0000000000000..bda5b8ee08af4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "MyWebsocketApiEBAC53DF": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyWebsocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts new file mode 100644 index 0000000000000..e9acb82402cfa --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets'); + +new apigw.WebSocketApi(stack, 'MyWebsocketApi'); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts index 07eadc5300a85..63e82bc579552 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -41,6 +41,44 @@ describe('WebSocketRoute', () => { IntegrationUri: 'some-uri', }); }); + + test('Api Key is required for route when apiKeyIsRequired is true', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + apiKeyRequired: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.apiId), + ApiKeyRequired: true, + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'RouteWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f20338431E', + }, + ], + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.apiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); });