Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apigatewayv2): http api - IAM authorizer support #17519

Merged
merged 55 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
01ee864
feat(apigatewayv2): add AWS_IAM authorizer support
misterjoshua Nov 16, 2021
b036ab6
refactor: use ?: optional syntax
misterjoshua Nov 16, 2021
c928252
refactor: make httpMethod a list
misterjoshua Nov 16, 2021
7651962
refactor: remove unneeded else block
misterjoshua Nov 16, 2021
f68870e
chore: expand on why authorizationType must be NONE
misterjoshua Nov 16, 2021
7baa434
refactor: add a safety error when iam is enabled and an authorizer set
misterjoshua Nov 16, 2021
a83a262
feat: add HttpIamAuthorizer
misterjoshua Nov 16, 2021
7190cbd
chore: update integ stack verification steps
misterjoshua Nov 16, 2021
7275bfa
docs: adjust some wording
misterjoshua Nov 16, 2021
7384149
remove enableIamAuthorization in favor of HttpIamAuthorization
misterjoshua Nov 16, 2021
dad0441
refactor: simplify the path variable logic to a replace
misterjoshua Nov 16, 2021
9e4440d
fix: add a single-trailing-newline to the readme
misterjoshua Nov 16, 2021
b8c6f3f
chore: improve the wording of the httpMethod option docs
misterjoshua Nov 16, 2021
6af5796
fix: iam authorizer example passes yarn rosetta:extract --strict
misterjoshua Nov 16, 2021
cf677de
fix: make the grantInvoke example work with rosetta:extract --strict
misterjoshua Nov 16, 2021
d8fa36e
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 17, 2021
1b36d28
refactor: better filename for iam authorizer
misterjoshua Nov 18, 2021
3326856
refactor: move the iam authorizer out of the authorizers package
misterjoshua Nov 18, 2021
1a18251
chore: remove the last of the other-package changes
misterjoshua Nov 18, 2021
e15ad8b
fix: export HttpIamAuthorizer so it is available alongside grantInvoke
misterjoshua Nov 18, 2021
b061e12
docs: add a grantInvoke example with defaultAuthorizer & integ test
misterjoshua Nov 20, 2021
876a361
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 20, 2021
962c301
refactor: change httpMethod -> httpMethods
misterjoshua Nov 20, 2021
48eaa71
fix: doc example no longerr gives rosetta warning
misterjoshua Nov 20, 2021
ec66563
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 21, 2021
fc5f640
feat: add routeArn
misterjoshua Nov 22, 2021
1ff91a6
fix: add th emissing @attribute doctag
misterjoshua Nov 22, 2021
de50380
refactor: change grantInvoke's default httpMethods to the route's method
misterjoshua Nov 22, 2021
58d1596
chore: remove unnecessary produceRouteArn default arg
misterjoshua Nov 22, 2021
a9875db
fix: align the title of the test with what it actually tests
misterjoshua Nov 22, 2021
b14b947
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 22, 2021
df4bfaf
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 25, 2021
089f7ba
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 26, 2021
ba92c36
Merge remote-tracking branch 'origin/master' into add-iam-authorizer
misterjoshua Nov 26, 2021
7517d0b
chore: test and update the integ test
misterjoshua Nov 26, 2021
58f1e26
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 26, 2021
11bdde5
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 29, 2021
6dc6fb1
chore: update the integ test so it works with 29039e8
misterjoshua Nov 29, 2021
d250a74
Merge branch 'master' into add-iam-authorizer
misterjoshua Nov 30, 2021
bf7e077
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 1, 2021
d31de47
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 2, 2021
5bd8bf6
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 3, 2021
488fd39
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 4, 2021
f546da0
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 5, 2021
1afe120
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 6, 2021
63bb1ff
refactor: review changes
misterjoshua Dec 7, 2021
c9dc5b1
refactor: further simplify HttpRouteKey
misterjoshua Dec 7, 2021
2d11bc5
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 13, 2021
b487850
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 14, 2021
007b1bd
Merge branch 'master' into add-iam-authorizer
misterjoshua Dec 16, 2021
fd3e10a
refactor: move the authorizer to the -authorizers package
misterjoshua Dec 17, 2021
e1e4a73
chore: remove some of the extraneous changes compared to master
misterjoshua Dec 17, 2021
8452922
chore: further cleanups
misterjoshua Dec 17, 2021
90ce8cf
chore: fix the docs
misterjoshua Dec 17, 2021
fb51b90
Merge branch 'master' into add-iam-authorizer
mergify[bot] Dec 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@ API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-acces

These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library.

In addition to these authorizers, API Gateway supports IAM via the included `HttpIamAuthorizer` and grant syntax:

```ts
declare const principal: iam.IGrantee;

const authorizer = new apigwv2.HttpIamAuthorizer();

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

const routes = httpApi.addRoute({
misterjoshua marked this conversation as resolved.
Show resolved Hide resolved
integration,
path: '/books/{book}',
});

routes[0].grantInvoke(principal);
```

### Metrics

The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch.
Expand Down
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/iam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IHttpRouteAuthorizer, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig } from './authorizer';

/**
* Authorize HTTP API Routes with IAM
*/
export class HttpIamAuthorizer implements IHttpRouteAuthorizer {
public bind(_options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig {
return {
authorizationType: 'AWS_IAM',
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './integration';
export * from './stage';
export * from './vpc-link';
export * from './authorizer';
export * from './iam';
158 changes: 129 additions & 29 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Resource } from '@aws-cdk/core';
import * as iam from '@aws-cdk/aws-iam';
import { Resource, Lazy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnRoute, CfnRouteProps } from '../apigatewayv2.generated';
import { IRoute } from '../common';
import { IHttpApi } from './api';
import { IHttpRouteAuthorizer } from './authorizer';
import { HttpRouteAuthorizerConfig, IHttpRouteAuthorizer } from './authorizer';
import { HttpIamAuthorizer } from './iam';
import { HttpRouteIntegration } from './integration';

/**
Expand All @@ -19,6 +21,28 @@ export interface IHttpRoute extends IRoute {
* Returns the path component of this HTTP route, `undefined` if the path is the catch-all route.
*/
readonly path?: string;

/**
* Returns the arn of the route.
* @attribute
*/
readonly routeArn: string;

/**
* Grant access to invoke the route.
misterjoshua marked this conversation as resolved.
Show resolved Hide resolved
*/
grantInvoke(grantee: iam.IGrantable, options?: GrantInvokeOptions): iam.Grant;
}

/**
* Options for granting invoke access.
*/
export interface GrantInvokeOptions {
/**
* The HTTP methods to allow.
* @default - the HttpMethod of the route
*/
readonly httpMethods?: HttpMethod[];
}

/**
Expand Down Expand Up @@ -51,7 +75,7 @@ export class HttpRouteKey {
/**
* The catch-all route of the API, i.e., when no other routes match
*/
public static readonly DEFAULT = new HttpRouteKey('$default');
public static readonly DEFAULT = new HttpRouteKey(HttpMethod.ANY, '$default');

/**
* Create a route key with the combination of the path and the method.
Expand All @@ -61,9 +85,14 @@ export class HttpRouteKey {
if (path !== '/' && (!path.startsWith('/') || path.endsWith('/'))) {
throw new Error('A route path must always start with a "/" and not end with a "/"');
}
return new HttpRouteKey(`${method ?? HttpMethod.ANY} ${path}`, path);
const keyMethod = method ?? HttpMethod.ANY;
return new HttpRouteKey(keyMethod, `${keyMethod} ${path}`, path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Now that key can be derived from the other two attributes, I would remove it from the constructor to make it cleaner and avoid mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now simplified this section so that it's cleaner and easier to avoid mistakes. As far as I can see, HTTP API has only the $default and METHOD /path/here formats for key, so this should work. https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html

Let me know whether this change works for you.

}

/**
* The method of the route
*/
public readonly method: HttpMethod;
/**
* The key to the RouteKey as recognized by APIGateway
*/
Expand All @@ -74,7 +103,8 @@ export class HttpRouteKey {
*/
public readonly path?: string;

private constructor(key: string, path?: string) {
private constructor(method: HttpMethod, key: string, path?: string) {
this.method = method;
this.key = key;
this.path = path;
}
Expand Down Expand Up @@ -124,6 +154,9 @@ export interface HttpRouteProps extends BatchHttpRouteOptions {
* Supported Route Authorizer types
*/
enum HttpRouteAuthorizationType {
/** AWS IAM */
AWS_IAM = 'AWS_IAM',

/** JSON Web Tokens */
JWT = 'JWT',

Expand All @@ -142,50 +175,117 @@ export class HttpRoute extends Resource implements IHttpRoute {
public readonly routeId: string;
public readonly httpApi: IHttpApi;
public readonly path?: string;
public readonly routeArn: string;

private readonly method: HttpMethod;
private authorizer?: IHttpRouteAuthorizer;
private authBindResult?: HttpRouteAuthorizerConfig;

constructor(scope: Construct, id: string, props: HttpRouteProps) {
super(scope, id);

this.httpApi = props.httpApi;
this.path = props.routeKey.path;
this.method = props.routeKey.method;
this.routeArn = this.produceRouteArn(props.routeKey.method);
this.authorizer = props.authorizer;

const config = props.integration._bindToRoute({
route: this,
scope: this,
});

const authBindResult = props.authorizer ? props.authorizer.bind({
route: this,
scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported
}) : undefined;

if (authBindResult && !(authBindResult.authorizationType in HttpRouteAuthorizationType)) {
throw new Error('authorizationType should either be JWT, CUSTOM, or NONE');
}

let authorizationScopes = authBindResult?.authorizationScopes;

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

if (authorizationScopes?.length === 0) {
authorizationScopes = undefined;
}
// Bind early to emit any exceptions as early as possible.
this.tryAuthorizerBinding();

const routeProps: CfnRouteProps = {
apiId: props.httpApi.apiId,
routeKey: props.routeKey.key,
target: `integrations/${config.integrationId}`,
authorizerId: authBindResult?.authorizerId,
authorizationType: authBindResult?.authorizationType ?? 'NONE', // must be explicitly NONE (not undefined) for stack updates to work correctly
authorizationScopes,
authorizerId: Lazy.string({ produce: () => this.authBindResult?.authorizerId }),
authorizationType: Lazy.string({
produce: () => {
// Provide the authorization type or explicitly 'NONE'. We must use
// 'NONE' and not undefined or CloudFormation doesn't remove an
// authorizer. @see https://github.com/aws/aws-cdk/pull/14424
return this.authBindResult?.authorizationType ?? 'NONE';
},
}),
authorizationScopes: Lazy.list({
produce: () => {
let authorizationScopes = this.authBindResult?.authorizationScopes;

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

if (authorizationScopes?.length === 0) {
authorizationScopes = undefined;
}

return authorizationScopes;
},
}),
};

const route = new CfnRoute(this, 'Resource', routeProps);
this.routeId = route.ref;
}

protected onPrepare() {
super.onPrepare();
this.tryAuthorizerBinding();
}

private tryAuthorizerBinding() {
if (this.authorizer && !this.authBindResult) {
this.authBindResult = this.authorizer.bind({
route: this,
scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported
});

if (!(this.authBindResult.authorizationType in HttpRouteAuthorizationType)) {
throw new Error('authorizationType should either be AWS_IAM, JWT, CUSTOM, or NONE');
}
}
}

private produceRouteArn(httpMethod: HttpMethod): string {
const stage = '*';
const iamHttpMethod = httpMethod === HttpMethod.ANY ? '*' : httpMethod;
const path = this.path ?? '/';
// When the user has provided a path with path variables, we replace the
// path variable and all that follows with a wildcard.
const iamPath = path.replace(/\{.*?\}.*/, '*');

return `arn:aws:execute-api:${this.stack.region}:${this.stack.account}:${this.httpApi.apiId}/${stage}/${iamHttpMethod}${iamPath}`;
}
misterjoshua marked this conversation as resolved.
Show resolved Hide resolved

public grantInvoke(grantee: iam.IGrantable, options: GrantInvokeOptions = {}): iam.Grant {
if (this.authorizer && !(this.authorizer instanceof HttpIamAuthorizer)) {
throw new Error('The authorizer has been set, so we cannot enable IAM authorization');
}

if (!this.authorizer) {
this.authorizer = new HttpIamAuthorizer();
}

const httpMethods = Array.from(new Set(options.httpMethods ?? [this.method]));
if (this.method !== HttpMethod.ANY && httpMethods.some(method => method !== this.method)) {
throw new Error('This route does not support granting invoke for all requested http methods');
}

const resourceArns = httpMethods.map(httpMethod => {
return this.produceRouteArn(httpMethod);
});

return iam.Grant.addToPrincipal({
grantee,
actions: ['execute-api:Invoke'],
resourceArns: resourceArns,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Construct } from 'constructs';
import { Duration, Stack } from '@aws-cdk/core';
import * as apigwv2 from '@aws-cdk/aws-apigatewayv2';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
Expand Down
Loading