Skip to content

Commit

Permalink
fix(apigatewayv2): defaultAuthorizer cannot be applied to HttpRoute (#…
Browse files Browse the repository at this point in the history
…27576)

This PR fixes a bug that `defaultAuthorizer` cannot be applied to `HttpRoute` without an authorizer.

Closes #27436.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
go-to-k authored and mrgrain committed Nov 1, 2023
1 parent b7ffc02 commit d1b2ca7
Show file tree
Hide file tree
Showing 13 changed files with 1,536 additions and 304 deletions.
27 changes: 23 additions & 4 deletions packages/@aws-cdk/aws-apigatewayv2-alpha/lib/http/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,29 @@ import { DomainMappingOptions } from '../common/stage';
export interface IHttpApi extends IApi {
/**
* The identifier of this API Gateway HTTP API.
*
* @attribute
* @deprecated - use apiId instead
*/
readonly httpApiId: string;

/**
* Default Authorizer applied to all routes in the gateway.
*
* @attribute
* @default - no default authorizer
*/
readonly defaultAuthorizer?: IHttpRouteAuthorizer;

/**
* Default OIDC scopes attached to all routes in the gateway, unless explicitly configured on the route.
* The scopes are used with a COGNITO_USER_POOLS authorizer to authorize the method invocation.
*
* @attribute
* @default - no default authorization scopes
*/
readonly defaultAuthorizationScopes?: string[];

/**
* Metric for the number of client-side errors captured in a given period.
*
Expand Down Expand Up @@ -125,14 +143,15 @@ export interface HttpApiProps {
readonly disableExecuteApiEndpoint?: boolean;

/**
* Default Authorizer to applied to all routes in the gateway
* Default Authorizer applied to all routes in the gateway.
*
* @default - No authorizer
* @default - no default authorizer
*/
readonly defaultAuthorizer?: IHttpRouteAuthorizer;

/**
* Default OIDC scopes attached to all routes in the gateway, unless explicitly configured on the route.
* The scopes are used with a COGNITO_USER_POOLS authorizer to authorize the method invocation.
*
* @default - no default authorization scopes
*/
Expand Down Expand Up @@ -340,8 +359,8 @@ export class HttpApi extends HttpApiBase {

private readonly _apiEndpoint: string;

private readonly defaultAuthorizer?: IHttpRouteAuthorizer;
private readonly defaultAuthorizationScopes?: string[];
public readonly defaultAuthorizer?: IHttpRouteAuthorizer;
public readonly defaultAuthorizationScopes?: string[];

constructor(scope: Construct, id: string, props?: HttpApiProps) {
super(scope, id);
Expand Down
7 changes: 4 additions & 3 deletions packages/@aws-cdk/aws-apigatewayv2-alpha/lib/http/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ export class HttpRoute extends Resource implements IHttpRoute {
scope: this,
});

this.authBindResult = props.authorizer?.bind({
const authorizer = props.authorizer ?? this.httpApi.defaultAuthorizer;
this.authBindResult = authorizer?.bind({
route: this,
scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported
});
Expand All @@ -204,10 +205,10 @@ export class HttpRoute extends Resource implements IHttpRoute {

let authorizationScopes = this.authBindResult?.authorizationScopes;

if (this.authBindResult && props.authorizationScopes) {
if (this.authBindResult && (props.authorizationScopes || this.httpApi.defaultAuthorizationScopes)) {
authorizationScopes = Array.from(new Set([
...authorizationScopes ?? [],
...props.authorizationScopes,
...props.authorizationScopes ?? this.httpApi.defaultAuthorizationScopes ?? [],
]));
}

Expand Down
90 changes: 90 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2-alpha/test/http/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,96 @@ describe('HttpRoute', () => {
});
});

test('can create route without an authorizer when api has defaultAuthorizer', () => {
const stack = new Stack();

const authorizer = new DummyAuthorizer();
const httpApi = new HttpApi(stack, 'HttpApi', {
defaultAuthorizer: authorizer,
defaultAuthorizationScopes: ['read:books'],
});

const route = new HttpRoute(stack, 'HttpRoute', {
httpApi,
integration: new DummyIntegration(),
routeKey: HttpRouteKey.with('/books', HttpMethod.GET),
});

Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', {
ApiId: stack.resolve(httpApi.apiId),
IntegrationType: 'HTTP_PROXY',
PayloadFormatVersion: '2.0',
IntegrationUri: 'some-uri',
});

Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Authorizer', 1);
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
AuthorizerId: stack.resolve(authorizer.bind({ scope: stack, route: route }).authorizerId),
AuthorizationType: 'JWT',
AuthorizationScopes: ['read:books'],
});
});

test('authorizationScopes can be applied to route without authorizer when api has defaultAuthorizer', () => {
const stack = new Stack();

const authorizer = new DummyAuthorizer();
const httpApi = new HttpApi(stack, 'HttpApi', {
defaultAuthorizer: authorizer,
});

const route = new HttpRoute(stack, 'HttpRoute', {
httpApi,
integration: new DummyIntegration(),
routeKey: HttpRouteKey.with('/books', HttpMethod.GET),
authorizationScopes: ['read:books'],
});

Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', {
ApiId: stack.resolve(httpApi.apiId),
IntegrationType: 'HTTP_PROXY',
PayloadFormatVersion: '2.0',
IntegrationUri: 'some-uri',
});

Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Authorizer', 1);
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
AuthorizerId: stack.resolve(authorizer.bind({ scope: stack, route: route }).authorizerId),
AuthorizationType: 'JWT',
AuthorizationScopes: ['read:books'],
});
});

test('defaultAuthorizationScopes can be applied to route', () => {
const stack = new Stack();

const authorizer = new DummyAuthorizer();
const httpApi = new HttpApi(stack, 'HttpApi', {
defaultAuthorizationScopes: ['read:books'],
});

const route = new HttpRoute(stack, 'HttpRoute', {
httpApi,
integration: new DummyIntegration(),
routeKey: HttpRouteKey.with('/books', HttpMethod.GET),
authorizer,
});

Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', {
ApiId: stack.resolve(httpApi.apiId),
IntegrationType: 'HTTP_PROXY',
PayloadFormatVersion: '2.0',
IntegrationUri: 'some-uri',
});

Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Authorizer', 1);
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
AuthorizerId: stack.resolve(authorizer.bind({ scope: stack, route: route }).authorizerId),
AuthorizationType: 'JWT',
AuthorizationScopes: ['read:books'],
});
});

test('can attach additional scopes to a route with an authorizer attached', () => {
const stack = new Stack();
const httpApi = new HttpApi(stack, 'HttpApi');
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d1b2ca7

Please sign in to comment.