@aligent/cdk-secure-rest-api 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +166 -0
- package/index.js +6 -0
- package/index.ts +7 -0
- package/jest.config.ts +11 -0
- package/lib/secure-rest-api.js +51 -0
- package/lib/secure-rest-api.test.ts +308 -0
- package/lib/secure-rest-api.ts +129 -0
- package/package.json +37 -0
- package/project.json +36 -0
- package/tsconfig.app.json +11 -0
- package/tsconfig.json +16 -0
- package/tsconfig.spec.json +8 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @aligent/cdk-secure-rest-api
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1667](https://github.com/aligent/cdk-constructs/pull/1667) [`631a92c`](https://github.com/aligent/cdk-constructs/commit/631a92ce3b8846a97c757a2145a369f287ef4ad3) Thanks [@toddhainsworth](https://github.com/toddhainsworth)! - Add new `SecureRestApi` construct for provisioning an API Gateway REST API secured with API key authentication and usage plan throttling. Supports configurable routes (accepting any CDK `Integration`), CORS options, and throttle limits.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#1669](https://github.com/aligent/cdk-constructs/pull/1669) [`8006ed3`](https://github.com/aligent/cdk-constructs/commit/8006ed327661e66d9e9b91b2d3ec205594ba4c06) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore(deps): bump the aws group across 1 directory with 9 updates
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Aligent AWS Secure REST API
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
  
|
|
6
|
+
|
|
7
|
+
A CDK construct for provisioning an API Gateway REST API secured with API Key authentication and usage plan throttling. Routes accept any CDK `Integration`, giving callers full control over how endpoints are wired.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- API Gateway REST API with API Key authentication (`apiKeyRequired: true` on all routes)
|
|
12
|
+
- Usage plan with configurable throttle rate and burst limits
|
|
13
|
+
- Configurable CORS preflight options
|
|
14
|
+
- Accepts any CDK `Integration` per route (Lambda, HTTP, Mock, Step Functions, etc.)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @aligent/cdk-secure-rest-api
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with yarn:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
yarn add @aligent/cdk-secure-rest-api aws-cdk-lib constructs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Peer Dependencies
|
|
29
|
+
|
|
30
|
+
- `aws-cdk-lib` (^2.113.0)
|
|
31
|
+
- `constructs` (^10.5.0)
|
|
32
|
+
|
|
33
|
+
## Basic Usage
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { SecureRestApi } from '@aligent/cdk-secure-rest-api';
|
|
37
|
+
import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
|
|
38
|
+
import { HttpMethod } from 'aws-cdk-lib/aws-apigatewayv2';
|
|
39
|
+
|
|
40
|
+
const api = new SecureRestApi(this, 'Api', {
|
|
41
|
+
apiName: 'my-api',
|
|
42
|
+
routes: [
|
|
43
|
+
{
|
|
44
|
+
path: 'items',
|
|
45
|
+
methods: [HttpMethod.GET],
|
|
46
|
+
integration: new LambdaIntegration(myFunction),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Access created resources
|
|
52
|
+
const { api, apiKey, usagePlan } = api;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration Examples
|
|
56
|
+
|
|
57
|
+
### Multiple routes and methods
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const api = new SecureRestApi(this, 'Api', {
|
|
61
|
+
apiName: 'my-api',
|
|
62
|
+
routes: [
|
|
63
|
+
{
|
|
64
|
+
path: 'items',
|
|
65
|
+
methods: [HttpMethod.GET, HttpMethod.POST],
|
|
66
|
+
integration: new LambdaIntegration(itemsFunction),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: 'orders',
|
|
70
|
+
methods: [HttpMethod.GET],
|
|
71
|
+
integration: new LambdaIntegration(ordersFunction),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Custom throttling
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const api = new SecureRestApi(this, 'Api', {
|
|
81
|
+
apiName: 'my-api',
|
|
82
|
+
throttle: {
|
|
83
|
+
rateLimit: 50, // requests per second
|
|
84
|
+
burstLimit: 100,
|
|
85
|
+
},
|
|
86
|
+
routes: [...],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Custom CORS configuration
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const api = new SecureRestApi(this, 'Api', {
|
|
94
|
+
apiName: 'my-api',
|
|
95
|
+
corsOptions: {
|
|
96
|
+
allowOrigins: ['https://example.com'], // overrides default (all origins)
|
|
97
|
+
additionalMethods: ['POST'], // appended to GET, OPTIONS
|
|
98
|
+
additionalHeaders: ['Authorization'], // appended to Content-Type, X-Api-Key
|
|
99
|
+
},
|
|
100
|
+
routes: [...],
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Configuration Reference
|
|
105
|
+
|
|
106
|
+
### `apiName` (string) — required
|
|
107
|
+
|
|
108
|
+
The name of the REST API and the base for generated resource names.
|
|
109
|
+
|
|
110
|
+
### `description` (string)
|
|
111
|
+
|
|
112
|
+
Description for the API Gateway REST API.
|
|
113
|
+
|
|
114
|
+
Default: `REST API for {apiName} service`
|
|
115
|
+
|
|
116
|
+
### `routes` (SecureRestApiRoute[]) — required
|
|
117
|
+
|
|
118
|
+
Routes to register on the API. Each route requires:
|
|
119
|
+
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
|----------|------|-------------|
|
|
122
|
+
| `path` | `string` | The resource path (leading slash is stripped automatically) |
|
|
123
|
+
| `methods` | `HttpMethod[]` | HTTP methods to register on the resource |
|
|
124
|
+
| `integration` | `Integration` | Any CDK API Gateway integration |
|
|
125
|
+
|
|
126
|
+
### `throttle` (object)
|
|
127
|
+
|
|
128
|
+
Throttling limits applied to the usage plan.
|
|
129
|
+
|
|
130
|
+
| Property | Type | Default |
|
|
131
|
+
|----------|------|---------|
|
|
132
|
+
| `rateLimit` | `number` | `100` |
|
|
133
|
+
| `burstLimit` | `number` | `200` |
|
|
134
|
+
|
|
135
|
+
### `corsOptions` (object)
|
|
136
|
+
|
|
137
|
+
CORS preflight configuration applied to all resources.
|
|
138
|
+
|
|
139
|
+
| Property | Type | Behaviour |
|
|
140
|
+
|----------|------|-----------|
|
|
141
|
+
| `allowOrigins` | `string[]` | Overrides the default (all origins) |
|
|
142
|
+
| `additionalMethods` | `string[]` | Appended to the defaults: `GET`, `OPTIONS` |
|
|
143
|
+
| `additionalHeaders` | `string[]` | Appended to the defaults: `Content-Type`, `X-Api-Key` |
|
|
144
|
+
|
|
145
|
+
### `apiKeyName` (string)
|
|
146
|
+
|
|
147
|
+
Override the name of the generated API key.
|
|
148
|
+
|
|
149
|
+
Default: `{apiName}-api-key`
|
|
150
|
+
|
|
151
|
+
### `usagePlanName` (string)
|
|
152
|
+
|
|
153
|
+
Override the name of the generated usage plan.
|
|
154
|
+
|
|
155
|
+
Default: `{apiName}-usage-plan`
|
|
156
|
+
|
|
157
|
+
## Local Development
|
|
158
|
+
|
|
159
|
+
[NPM link](https://docs.npmjs.com/cli/v7/commands/npm-link) can be used to develop the module locally:
|
|
160
|
+
|
|
161
|
+
1. Pull this repository locally
|
|
162
|
+
2. `cd` into this repository
|
|
163
|
+
3. Run `npm link`
|
|
164
|
+
4. `cd` into the downstream repo and run `npm link '@aligent/cdk-secure-rest-api'`
|
|
165
|
+
|
|
166
|
+
The downstream repository should now include a symlink to this module, allowing local changes to be tested before pushing.
|
package/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecureRestApi = void 0;
|
|
4
|
+
const secure_rest_api_1 = require("./lib/secure-rest-api");
|
|
5
|
+
Object.defineProperty(exports, "SecureRestApi", { enumerable: true, get: function () { return secure_rest_api_1.SecureRestApi; } });
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwyREFJK0I7QUFFdEIsOEZBTFAsK0JBQWEsT0FLTyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIFNlY3VyZVJlc3RBcGksXG4gIFNlY3VyZVJlc3RBcGlQcm9wcyxcbiAgU2VjdXJlUmVzdEFwaVJvdXRlLFxufSBmcm9tIFwiLi9saWIvc2VjdXJlLXJlc3QtYXBpXCI7XG5cbmV4cG9ydCB7IFNlY3VyZVJlc3RBcGksIFNlY3VyZVJlc3RBcGlQcm9wcywgU2VjdXJlUmVzdEFwaVJvdXRlIH07XG4iXX0=
|
package/index.ts
ADDED
package/jest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: "secure-rest-api",
|
|
4
|
+
preset: "../../../jest.preset.js",
|
|
5
|
+
testEnvironment: "node",
|
|
6
|
+
transform: {
|
|
7
|
+
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ["ts", "js", "html"],
|
|
10
|
+
coverageDirectory: "../../../coverage/packages/secure-rest-api",
|
|
11
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecureRestApi = void 0;
|
|
4
|
+
const aws_apigateway_1 = require("aws-cdk-lib/aws-apigateway");
|
|
5
|
+
const constructs_1 = require("constructs");
|
|
6
|
+
class SecureRestApi extends constructs_1.Construct {
|
|
7
|
+
constructor(scope, id, props) {
|
|
8
|
+
var _a, _b, _c;
|
|
9
|
+
super(scope, id);
|
|
10
|
+
const { apiName, description, corsOptions, routes, throttle = { rateLimit: 100, burstLimit: 200 }, apiKeyName, usagePlanName, } = props;
|
|
11
|
+
this.api = new aws_apigateway_1.RestApi(this, "Api", {
|
|
12
|
+
restApiName: apiName,
|
|
13
|
+
description: description !== null && description !== void 0 ? description : `REST API for ${apiName} service`,
|
|
14
|
+
defaultCorsPreflightOptions: {
|
|
15
|
+
allowOrigins: (_a = corsOptions === null || corsOptions === void 0 ? void 0 : corsOptions.allowOrigins) !== null && _a !== void 0 ? _a : aws_apigateway_1.Cors.ALL_ORIGINS,
|
|
16
|
+
allowMethods: [
|
|
17
|
+
"GET",
|
|
18
|
+
"OPTIONS",
|
|
19
|
+
...((_b = corsOptions === null || corsOptions === void 0 ? void 0 : corsOptions.additionalMethods) !== null && _b !== void 0 ? _b : []),
|
|
20
|
+
],
|
|
21
|
+
allowHeaders: [
|
|
22
|
+
"Content-Type",
|
|
23
|
+
"X-Api-Key",
|
|
24
|
+
...((_c = corsOptions === null || corsOptions === void 0 ? void 0 : corsOptions.additionalHeaders) !== null && _c !== void 0 ? _c : []),
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
for (const route of routes) {
|
|
29
|
+
const resource = this.api.root.addResource(route.path.replace(/^\//, ""));
|
|
30
|
+
for (const method of route.methods) {
|
|
31
|
+
resource.addMethod(method, route.integration, { apiKeyRequired: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.apiKey = new aws_apigateway_1.ApiKey(this, "ApiKey", {
|
|
35
|
+
description: `API Key for ${apiName} service`,
|
|
36
|
+
apiKeyName: apiKeyName !== null && apiKeyName !== void 0 ? apiKeyName : `${apiName}-api-key`,
|
|
37
|
+
});
|
|
38
|
+
this.usagePlan = new aws_apigateway_1.UsagePlan(this, "UsagePlan", {
|
|
39
|
+
name: usagePlanName !== null && usagePlanName !== void 0 ? usagePlanName : `${apiName}-usage-plan`,
|
|
40
|
+
description: `Usage plan for ${apiName} service`,
|
|
41
|
+
throttle,
|
|
42
|
+
});
|
|
43
|
+
this.usagePlan.addApiStage({
|
|
44
|
+
api: this.api,
|
|
45
|
+
stage: this.api.deploymentStage,
|
|
46
|
+
});
|
|
47
|
+
this.usagePlan.addApiKey(this.apiKey);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.SecureRestApi = SecureRestApi;
|
|
51
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdXJlLXJlc3QtYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic2VjdXJlLXJlc3QtYXBpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLCtEQU9vQztBQUVwQywyQ0FBdUM7QUEyRHZDLE1BQWEsYUFBYyxTQUFRLHNCQUFTO0lBSzFDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBeUI7O1FBQ2pFLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFFakIsTUFBTSxFQUNKLE9BQU8sRUFDUCxXQUFXLEVBQ1gsV0FBVyxFQUNYLE1BQU0sRUFDTixRQUFRLEdBQUcsRUFBRSxTQUFTLEVBQUUsR0FBRyxFQUFFLFVBQVUsRUFBRSxHQUFHLEVBQUUsRUFDOUMsVUFBVSxFQUNWLGFBQWEsR0FDZCxHQUFHLEtBQUssQ0FBQztRQUVWLElBQUksQ0FBQyxHQUFHLEdBQUcsSUFBSSx3QkFBTyxDQUFDLElBQUksRUFBRSxLQUFLLEVBQUU7WUFDbEMsV0FBVyxFQUFFLE9BQU87WUFDcEIsV0FBVyxFQUFFLFdBQVcsYUFBWCxXQUFXLGNBQVgsV0FBVyxHQUFJLGdCQUFnQixPQUFPLFVBQVU7WUFDN0QsMkJBQTJCLEVBQUU7Z0JBQzNCLFlBQVksRUFBRSxNQUFBLFdBQVcsYUFBWCxXQUFXLHVCQUFYLFdBQVcsQ0FBRSxZQUFZLG1DQUFJLHFCQUFJLENBQUMsV0FBVztnQkFDM0QsWUFBWSxFQUFFO29CQUNaLEtBQUs7b0JBQ0wsU0FBUztvQkFDVCxHQUFHLENBQUMsTUFBQSxXQUFXLGFBQVgsV0FBVyx1QkFBWCxXQUFXLENBQUUsaUJBQWlCLG1DQUFJLEVBQUUsQ0FBQztpQkFDMUM7Z0JBQ0QsWUFBWSxFQUFFO29CQUNaLGNBQWM7b0JBQ2QsV0FBVztvQkFDWCxHQUFHLENBQUMsTUFBQSxXQUFXLGFBQVgsV0FBVyx1QkFBWCxXQUFXLENBQUUsaUJBQWlCLG1DQUFJLEVBQUUsQ0FBQztpQkFDMUM7YUFDRjtTQUNGLENBQUMsQ0FBQztRQUVILEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxFQUFFLENBQUM7WUFDM0IsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO1lBQzFFLEtBQUssTUFBTSxNQUFNLElBQUksS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDO2dCQUNuQyxRQUFRLENBQUMsU0FBUyxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsV0FBVyxFQUFFLEVBQUUsY0FBYyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7WUFDMUUsQ0FBQztRQUNILENBQUM7UUFFRCxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksdUJBQU0sQ0FBQyxJQUFJLEVBQUUsUUFBUSxFQUFFO1lBQ3ZDLFdBQVcsRUFBRSxlQUFlLE9BQU8sVUFBVTtZQUM3QyxVQUFVLEVBQUUsVUFBVSxhQUFWLFVBQVUsY0FBVixVQUFVLEdBQUksR0FBRyxPQUFPLFVBQVU7U0FDL0MsQ0FBQyxDQUFDO1FBRUgsSUFBSSxDQUFDLFNBQVMsR0FBRyxJQUFJLDBCQUFTLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRTtZQUNoRCxJQUFJLEVBQUUsYUFBYSxhQUFiLGFBQWEsY0FBYixhQUFhLEdBQUksR0FBRyxPQUFPLGFBQWE7WUFDOUMsV0FBVyxFQUFFLGtCQUFrQixPQUFPLFVBQVU7WUFDaEQsUUFBUTtTQUNULENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDO1lBQ3pCLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBZTtZQUN6QixLQUFLLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxlQUFlO1NBQ2hDLENBQUMsQ0FBQztRQUNILElBQUksQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUN4QyxDQUFDO0NBQ0Y7QUE1REQsc0NBNERDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHtcbiAgQXBpS2V5LFxuICBDb3JzLFxuICBJbnRlZ3JhdGlvbixcbiAgSVJlc3RBcGksXG4gIFJlc3RBcGksXG4gIFVzYWdlUGxhbixcbn0gZnJvbSBcImF3cy1jZGstbGliL2F3cy1hcGlnYXRld2F5XCI7XG5pbXBvcnQgeyBIdHRwTWV0aG9kIH0gZnJvbSBcImF3cy1jZGstbGliL2F3cy1hcGlnYXRld2F5djJcIjtcbmltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2VjdXJlUmVzdEFwaVJvdXRlIHtcbiAgcGF0aDogc3RyaW5nO1xuICBtZXRob2RzOiBIdHRwTWV0aG9kW107XG4gIGludGVncmF0aW9uOiBJbnRlZ3JhdGlvbjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTZWN1cmVSZXN0QXBpUHJvcHMge1xuICAvKipcbiAgICogVGhlIG5hbWUgb2YgdGhlIEFQSS5cbiAgICovXG4gIGFwaU5hbWU6IHN0cmluZztcblxuICAvKipcbiAgICogRGVzY3JpcHRpb24gZm9yIHRoZSBBUEkuXG4gICAqL1xuICBkZXNjcmlwdGlvbj86IHN0cmluZztcblxuICAvKipcbiAgICogQ09SUyBjb25maWd1cmF0aW9uLlxuICAgKlxuICAgKiBgYWxsb3dPcmlnaW5zYCBvdmVycmlkZXMgdGhlIGRlZmF1bHQgKGFsbCBvcmlnaW5zKS5cbiAgICogYGFkZGl0aW9uYWxNZXRob2RzYCBhbmQgYGFkZGl0aW9uYWxIZWFkZXJzYCBhcmUgYXBwZW5kZWQgdG8gdGhlIGRlZmF1bHRzXG4gICAqIChHRVQvT1BUSU9OUyBhbmQgQ29udGVudC1UeXBlL1gtQXBpLUtleSByZXNwZWN0aXZlbHkpLlxuICAgKi9cbiAgY29yc09wdGlvbnM/OiB7XG4gICAgYWxsb3dPcmlnaW5zPzogc3RyaW5nW107XG4gICAgYWRkaXRpb25hbE1ldGhvZHM/OiBzdHJpbmdbXTtcbiAgICBhZGRpdGlvbmFsSGVhZGVycz86IHN0cmluZ1tdO1xuICB9O1xuXG4gIC8qKlxuICAgKiBSb3V0ZXMgdG8gcmVnaXN0ZXIgb24gdGhlIEFQSS5cbiAgICovXG4gIHJvdXRlczogU2VjdXJlUmVzdEFwaVJvdXRlW107XG5cbiAgLyoqXG4gICAqIFRocm90dGxpbmcgbGltaXRzIGZvciB0aGUgdXNhZ2UgcGxhbi5cbiAgICogQGRlZmF1bHQgeyByYXRlTGltaXQ6IDEwMCwgYnVyc3RMaW1pdDogMjAwIH1cbiAgICovXG4gIHRocm90dGxlPzoge1xuICAgIHJhdGVMaW1pdDogbnVtYmVyO1xuICAgIGJ1cnN0TGltaXQ6IG51bWJlcjtcbiAgfTtcblxuICAvKipcbiAgICogT3ZlcnJpZGUgdGhlIGdlbmVyYXRlZCBBUEkga2V5IG5hbWUuXG4gICAqIEBkZWZhdWx0IGB7YXBpTmFtZX0tYXBpLWtleWBcbiAgICovXG4gIGFwaUtleU5hbWU/OiBzdHJpbmc7XG5cbiAgLyoqXG4gICAqIE92ZXJyaWRlIHRoZSBnZW5lcmF0ZWQgdXNhZ2UgcGxhbiBuYW1lLlxuICAgKiBAZGVmYXVsdCBge2FwaU5hbWV9LXVzYWdlLXBsYW5gXG4gICAqL1xuICB1c2FnZVBsYW5OYW1lPzogc3RyaW5nO1xufVxuXG5leHBvcnQgY2xhc3MgU2VjdXJlUmVzdEFwaSBleHRlbmRzIENvbnN0cnVjdCB7XG4gIHB1YmxpYyByZWFkb25seSBhcGk6IFJlc3RBcGk7XG4gIHB1YmxpYyByZWFkb25seSBhcGlLZXk6IEFwaUtleTtcbiAgcHVibGljIHJlYWRvbmx5IHVzYWdlUGxhbjogVXNhZ2VQbGFuO1xuXG4gIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzOiBTZWN1cmVSZXN0QXBpUHJvcHMpIHtcbiAgICBzdXBlcihzY29wZSwgaWQpO1xuXG4gICAgY29uc3Qge1xuICAgICAgYXBpTmFtZSxcbiAgICAgIGRlc2NyaXB0aW9uLFxuICAgICAgY29yc09wdGlvbnMsXG4gICAgICByb3V0ZXMsXG4gICAgICB0aHJvdHRsZSA9IHsgcmF0ZUxpbWl0OiAxMDAsIGJ1cnN0TGltaXQ6IDIwMCB9LFxuICAgICAgYXBpS2V5TmFtZSxcbiAgICAgIHVzYWdlUGxhbk5hbWUsXG4gICAgfSA9IHByb3BzO1xuXG4gICAgdGhpcy5hcGkgPSBuZXcgUmVzdEFwaSh0aGlzLCBcIkFwaVwiLCB7XG4gICAgICByZXN0QXBpTmFtZTogYXBpTmFtZSxcbiAgICAgIGRlc2NyaXB0aW9uOiBkZXNjcmlwdGlvbiA/PyBgUkVTVCBBUEkgZm9yICR7YXBpTmFtZX0gc2VydmljZWAsXG4gICAgICBkZWZhdWx0Q29yc1ByZWZsaWdodE9wdGlvbnM6IHtcbiAgICAgICAgYWxsb3dPcmlnaW5zOiBjb3JzT3B0aW9ucz8uYWxsb3dPcmlnaW5zID8/IENvcnMuQUxMX09SSUdJTlMsXG4gICAgICAgIGFsbG93TWV0aG9kczogW1xuICAgICAgICAgIFwiR0VUXCIsXG4gICAgICAgICAgXCJPUFRJT05TXCIsXG4gICAgICAgICAgLi4uKGNvcnNPcHRpb25zPy5hZGRpdGlvbmFsTWV0aG9kcyA/PyBbXSksXG4gICAgICAgIF0sXG4gICAgICAgIGFsbG93SGVhZGVyczogW1xuICAgICAgICAgIFwiQ29udGVudC1UeXBlXCIsXG4gICAgICAgICAgXCJYLUFwaS1LZXlcIixcbiAgICAgICAgICAuLi4oY29yc09wdGlvbnM/LmFkZGl0aW9uYWxIZWFkZXJzID8/IFtdKSxcbiAgICAgICAgXSxcbiAgICAgIH0sXG4gICAgfSk7XG5cbiAgICBmb3IgKGNvbnN0IHJvdXRlIG9mIHJvdXRlcykge1xuICAgICAgY29uc3QgcmVzb3VyY2UgPSB0aGlzLmFwaS5yb290LmFkZFJlc291cmNlKHJvdXRlLnBhdGgucmVwbGFjZSgvXlxcLy8sIFwiXCIpKTtcbiAgICAgIGZvciAoY29uc3QgbWV0aG9kIG9mIHJvdXRlLm1ldGhvZHMpIHtcbiAgICAgICAgcmVzb3VyY2UuYWRkTWV0aG9kKG1ldGhvZCwgcm91dGUuaW50ZWdyYXRpb24sIHsgYXBpS2V5UmVxdWlyZWQ6IHRydWUgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhpcy5hcGlLZXkgPSBuZXcgQXBpS2V5KHRoaXMsIFwiQXBpS2V5XCIsIHtcbiAgICAgIGRlc2NyaXB0aW9uOiBgQVBJIEtleSBmb3IgJHthcGlOYW1lfSBzZXJ2aWNlYCxcbiAgICAgIGFwaUtleU5hbWU6IGFwaUtleU5hbWUgPz8gYCR7YXBpTmFtZX0tYXBpLWtleWAsXG4gICAgfSk7XG5cbiAgICB0aGlzLnVzYWdlUGxhbiA9IG5ldyBVc2FnZVBsYW4odGhpcywgXCJVc2FnZVBsYW5cIiwge1xuICAgICAgbmFtZTogdXNhZ2VQbGFuTmFtZSA/PyBgJHthcGlOYW1lfS11c2FnZS1wbGFuYCxcbiAgICAgIGRlc2NyaXB0aW9uOiBgVXNhZ2UgcGxhbiBmb3IgJHthcGlOYW1lfSBzZXJ2aWNlYCxcbiAgICAgIHRocm90dGxlLFxuICAgIH0pO1xuXG4gICAgdGhpcy51c2FnZVBsYW4uYWRkQXBpU3RhZ2Uoe1xuICAgICAgYXBpOiB0aGlzLmFwaSBhcyBJUmVzdEFwaSxcbiAgICAgIHN0YWdlOiB0aGlzLmFwaS5kZXBsb3ltZW50U3RhZ2UsXG4gICAgfSk7XG4gICAgdGhpcy51c2FnZVBsYW4uYWRkQXBpS2V5KHRoaXMuYXBpS2V5KTtcbiAgfVxufVxuIl19
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { App, Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Template } from "aws-cdk-lib/assertions";
|
|
3
|
+
import { LambdaIntegration, MockIntegration } from "aws-cdk-lib/aws-apigateway";
|
|
4
|
+
import { HttpMethod } from "aws-cdk-lib/aws-apigatewayv2";
|
|
5
|
+
import { Function, InlineCode, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
|
+
import { Construct } from "constructs";
|
|
7
|
+
import { SecureRestApi } from "./secure-rest-api";
|
|
8
|
+
|
|
9
|
+
const createStack = () => {
|
|
10
|
+
const app = new App();
|
|
11
|
+
const stack = new Stack(app, "TestStack");
|
|
12
|
+
return { app, stack };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const mockIntegration = () =>
|
|
16
|
+
new MockIntegration({
|
|
17
|
+
integrationResponses: [{ statusCode: "200" }],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const lambdaIntegration = (scope: Construct) => {
|
|
21
|
+
const fn = new Function(scope, "Handler", {
|
|
22
|
+
runtime: Runtime.NODEJS_22_X,
|
|
23
|
+
handler: "index.handler",
|
|
24
|
+
code: InlineCode.fromInline("exports.handler = () => {}"),
|
|
25
|
+
});
|
|
26
|
+
return new LambdaIntegration(fn);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("SecureRestApi", () => {
|
|
30
|
+
describe("API Gateway", () => {
|
|
31
|
+
it("creates a REST API with the given name", () => {
|
|
32
|
+
const { stack } = createStack();
|
|
33
|
+
new SecureRestApi(stack, "Api", {
|
|
34
|
+
apiName: "my-api",
|
|
35
|
+
routes: [
|
|
36
|
+
{
|
|
37
|
+
path: "items",
|
|
38
|
+
methods: [HttpMethod.GET],
|
|
39
|
+
integration: mockIntegration(),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
45
|
+
"AWS::ApiGateway::RestApi",
|
|
46
|
+
{ Name: "my-api" }
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses the provided description", () => {
|
|
51
|
+
const { stack } = createStack();
|
|
52
|
+
new SecureRestApi(stack, "Api", {
|
|
53
|
+
apiName: "my-api",
|
|
54
|
+
description: "Custom description",
|
|
55
|
+
routes: [
|
|
56
|
+
{
|
|
57
|
+
path: "items",
|
|
58
|
+
methods: [HttpMethod.GET],
|
|
59
|
+
integration: mockIntegration(),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
65
|
+
"AWS::ApiGateway::RestApi",
|
|
66
|
+
{ Description: "Custom description" }
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("registers each route method with apiKeyRequired", () => {
|
|
71
|
+
const { stack } = createStack();
|
|
72
|
+
new SecureRestApi(stack, "Api", {
|
|
73
|
+
apiName: "my-api",
|
|
74
|
+
routes: [
|
|
75
|
+
{
|
|
76
|
+
path: "items",
|
|
77
|
+
methods: [HttpMethod.GET, HttpMethod.POST],
|
|
78
|
+
integration: mockIntegration(),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const template = Template.fromStack(stack);
|
|
84
|
+
template.hasResourceProperties("AWS::ApiGateway::Method", {
|
|
85
|
+
HttpMethod: "GET",
|
|
86
|
+
ApiKeyRequired: true,
|
|
87
|
+
});
|
|
88
|
+
template.hasResourceProperties("AWS::ApiGateway::Method", {
|
|
89
|
+
HttpMethod: "POST",
|
|
90
|
+
ApiKeyRequired: true,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("accepts a LambdaIntegration as route integration", () => {
|
|
95
|
+
const { stack } = createStack();
|
|
96
|
+
new SecureRestApi(stack, "Api", {
|
|
97
|
+
apiName: "my-api",
|
|
98
|
+
routes: [
|
|
99
|
+
{
|
|
100
|
+
path: "items",
|
|
101
|
+
methods: [HttpMethod.GET],
|
|
102
|
+
integration: lambdaIntegration(stack),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Template.fromStack(stack).resourceCountIs("AWS::Lambda::Function", 1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("API Key", () => {
|
|
112
|
+
it("creates an API key with a default name", () => {
|
|
113
|
+
const { stack } = createStack();
|
|
114
|
+
new SecureRestApi(stack, "Api", {
|
|
115
|
+
apiName: "my-api",
|
|
116
|
+
routes: [
|
|
117
|
+
{
|
|
118
|
+
path: "items",
|
|
119
|
+
methods: [HttpMethod.GET],
|
|
120
|
+
integration: mockIntegration(),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
126
|
+
"AWS::ApiGateway::ApiKey",
|
|
127
|
+
{ Name: "my-api-api-key" }
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("uses a custom API key name when provided", () => {
|
|
132
|
+
const { stack } = createStack();
|
|
133
|
+
new SecureRestApi(stack, "Api", {
|
|
134
|
+
apiName: "my-api",
|
|
135
|
+
apiKeyName: "custom-key",
|
|
136
|
+
routes: [
|
|
137
|
+
{
|
|
138
|
+
path: "items",
|
|
139
|
+
methods: [HttpMethod.GET],
|
|
140
|
+
integration: mockIntegration(),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
146
|
+
"AWS::ApiGateway::ApiKey",
|
|
147
|
+
{ Name: "custom-key" }
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("Usage Plan", () => {
|
|
153
|
+
it("creates a usage plan with default throttle settings", () => {
|
|
154
|
+
const { stack } = createStack();
|
|
155
|
+
new SecureRestApi(stack, "Api", {
|
|
156
|
+
apiName: "my-api",
|
|
157
|
+
routes: [
|
|
158
|
+
{
|
|
159
|
+
path: "items",
|
|
160
|
+
methods: [HttpMethod.GET],
|
|
161
|
+
integration: mockIntegration(),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
167
|
+
"AWS::ApiGateway::UsagePlan",
|
|
168
|
+
{
|
|
169
|
+
Throttle: {
|
|
170
|
+
BurstLimit: 200,
|
|
171
|
+
RateLimit: 100,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("uses custom throttle settings when provided", () => {
|
|
178
|
+
const { stack } = createStack();
|
|
179
|
+
new SecureRestApi(stack, "Api", {
|
|
180
|
+
apiName: "my-api",
|
|
181
|
+
throttle: { rateLimit: 50, burstLimit: 100 },
|
|
182
|
+
routes: [
|
|
183
|
+
{
|
|
184
|
+
path: "items",
|
|
185
|
+
methods: [HttpMethod.GET],
|
|
186
|
+
integration: mockIntegration(),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
192
|
+
"AWS::ApiGateway::UsagePlan",
|
|
193
|
+
{
|
|
194
|
+
Throttle: {
|
|
195
|
+
BurstLimit: 100,
|
|
196
|
+
RateLimit: 50,
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("uses a custom usage plan name when provided", () => {
|
|
203
|
+
const { stack } = createStack();
|
|
204
|
+
new SecureRestApi(stack, "Api", {
|
|
205
|
+
apiName: "my-api",
|
|
206
|
+
usagePlanName: "custom-plan",
|
|
207
|
+
routes: [
|
|
208
|
+
{
|
|
209
|
+
path: "items",
|
|
210
|
+
methods: [HttpMethod.GET],
|
|
211
|
+
integration: mockIntegration(),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
217
|
+
"AWS::ApiGateway::UsagePlan",
|
|
218
|
+
{ UsagePlanName: "custom-plan" }
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("CORS", () => {
|
|
224
|
+
it("applies default CORS settings", () => {
|
|
225
|
+
const { stack } = createStack();
|
|
226
|
+
new SecureRestApi(stack, "Api", {
|
|
227
|
+
apiName: "my-api",
|
|
228
|
+
routes: [
|
|
229
|
+
{
|
|
230
|
+
path: "items",
|
|
231
|
+
methods: [HttpMethod.GET],
|
|
232
|
+
integration: mockIntegration(),
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
238
|
+
"AWS::ApiGateway::Method",
|
|
239
|
+
{ HttpMethod: "OPTIONS" }
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("appends additionalHeaders to the defaults", () => {
|
|
244
|
+
const { stack } = createStack();
|
|
245
|
+
new SecureRestApi(stack, "Api", {
|
|
246
|
+
apiName: "my-api",
|
|
247
|
+
corsOptions: { additionalHeaders: ["Authorization"] },
|
|
248
|
+
routes: [
|
|
249
|
+
{
|
|
250
|
+
path: "items",
|
|
251
|
+
methods: [HttpMethod.GET],
|
|
252
|
+
integration: mockIntegration(),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// OPTIONS mock integration response headers include the allow-headers value
|
|
258
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
259
|
+
"AWS::ApiGateway::Method",
|
|
260
|
+
{
|
|
261
|
+
HttpMethod: "OPTIONS",
|
|
262
|
+
Integration: {
|
|
263
|
+
IntegrationResponses: [
|
|
264
|
+
{
|
|
265
|
+
ResponseParameters: {
|
|
266
|
+
"method.response.header.Access-Control-Allow-Headers":
|
|
267
|
+
"'Content-Type,X-Api-Key,Authorization'",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("appends additionalMethods to the defaults", () => {
|
|
277
|
+
const { stack } = createStack();
|
|
278
|
+
new SecureRestApi(stack, "Api", {
|
|
279
|
+
apiName: "my-api",
|
|
280
|
+
corsOptions: { additionalMethods: ["POST"] },
|
|
281
|
+
routes: [
|
|
282
|
+
{
|
|
283
|
+
path: "items",
|
|
284
|
+
methods: [HttpMethod.GET],
|
|
285
|
+
integration: mockIntegration(),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
Template.fromStack(stack).hasResourceProperties(
|
|
291
|
+
"AWS::ApiGateway::Method",
|
|
292
|
+
{
|
|
293
|
+
HttpMethod: "OPTIONS",
|
|
294
|
+
Integration: {
|
|
295
|
+
IntegrationResponses: [
|
|
296
|
+
{
|
|
297
|
+
ResponseParameters: {
|
|
298
|
+
"method.response.header.Access-Control-Allow-Methods":
|
|
299
|
+
"'GET,OPTIONS,POST'",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiKey,
|
|
3
|
+
Cors,
|
|
4
|
+
Integration,
|
|
5
|
+
IRestApi,
|
|
6
|
+
RestApi,
|
|
7
|
+
UsagePlan,
|
|
8
|
+
} from "aws-cdk-lib/aws-apigateway";
|
|
9
|
+
import { HttpMethod } from "aws-cdk-lib/aws-apigatewayv2";
|
|
10
|
+
import { Construct } from "constructs";
|
|
11
|
+
|
|
12
|
+
export interface SecureRestApiRoute {
|
|
13
|
+
path: string;
|
|
14
|
+
methods: HttpMethod[];
|
|
15
|
+
integration: Integration;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SecureRestApiProps {
|
|
19
|
+
/**
|
|
20
|
+
* The name of the API.
|
|
21
|
+
*/
|
|
22
|
+
apiName: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Description for the API.
|
|
26
|
+
*/
|
|
27
|
+
description?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CORS configuration.
|
|
31
|
+
*
|
|
32
|
+
* `allowOrigins` overrides the default (all origins).
|
|
33
|
+
* `additionalMethods` and `additionalHeaders` are appended to the defaults
|
|
34
|
+
* (GET/OPTIONS and Content-Type/X-Api-Key respectively).
|
|
35
|
+
*/
|
|
36
|
+
corsOptions?: {
|
|
37
|
+
allowOrigins?: string[];
|
|
38
|
+
additionalMethods?: string[];
|
|
39
|
+
additionalHeaders?: string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Routes to register on the API.
|
|
44
|
+
*/
|
|
45
|
+
routes: SecureRestApiRoute[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Throttling limits for the usage plan.
|
|
49
|
+
* @default { rateLimit: 100, burstLimit: 200 }
|
|
50
|
+
*/
|
|
51
|
+
throttle?: {
|
|
52
|
+
rateLimit: number;
|
|
53
|
+
burstLimit: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Override the generated API key name.
|
|
58
|
+
* @default `{apiName}-api-key`
|
|
59
|
+
*/
|
|
60
|
+
apiKeyName?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Override the generated usage plan name.
|
|
64
|
+
* @default `{apiName}-usage-plan`
|
|
65
|
+
*/
|
|
66
|
+
usagePlanName?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SecureRestApi extends Construct {
|
|
70
|
+
public readonly api: RestApi;
|
|
71
|
+
public readonly apiKey: ApiKey;
|
|
72
|
+
public readonly usagePlan: UsagePlan;
|
|
73
|
+
|
|
74
|
+
constructor(scope: Construct, id: string, props: SecureRestApiProps) {
|
|
75
|
+
super(scope, id);
|
|
76
|
+
|
|
77
|
+
const {
|
|
78
|
+
apiName,
|
|
79
|
+
description,
|
|
80
|
+
corsOptions,
|
|
81
|
+
routes,
|
|
82
|
+
throttle = { rateLimit: 100, burstLimit: 200 },
|
|
83
|
+
apiKeyName,
|
|
84
|
+
usagePlanName,
|
|
85
|
+
} = props;
|
|
86
|
+
|
|
87
|
+
this.api = new RestApi(this, "Api", {
|
|
88
|
+
restApiName: apiName,
|
|
89
|
+
description: description ?? `REST API for ${apiName} service`,
|
|
90
|
+
defaultCorsPreflightOptions: {
|
|
91
|
+
allowOrigins: corsOptions?.allowOrigins ?? Cors.ALL_ORIGINS,
|
|
92
|
+
allowMethods: [
|
|
93
|
+
"GET",
|
|
94
|
+
"OPTIONS",
|
|
95
|
+
...(corsOptions?.additionalMethods ?? []),
|
|
96
|
+
],
|
|
97
|
+
allowHeaders: [
|
|
98
|
+
"Content-Type",
|
|
99
|
+
"X-Api-Key",
|
|
100
|
+
...(corsOptions?.additionalHeaders ?? []),
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
for (const route of routes) {
|
|
106
|
+
const resource = this.api.root.addResource(route.path.replace(/^\//, ""));
|
|
107
|
+
for (const method of route.methods) {
|
|
108
|
+
resource.addMethod(method, route.integration, { apiKeyRequired: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.apiKey = new ApiKey(this, "ApiKey", {
|
|
113
|
+
description: `API Key for ${apiName} service`,
|
|
114
|
+
apiKeyName: apiKeyName ?? `${apiName}-api-key`,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.usagePlan = new UsagePlan(this, "UsagePlan", {
|
|
118
|
+
name: usagePlanName ?? `${apiName}-usage-plan`,
|
|
119
|
+
description: `Usage plan for ${apiName} service`,
|
|
120
|
+
throttle,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.usagePlan.addApiStage({
|
|
124
|
+
api: this.api as IRestApi,
|
|
125
|
+
stage: this.api.deploymentStage,
|
|
126
|
+
});
|
|
127
|
+
this.usagePlan.addApiKey(this.apiKey);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aligent/cdk-secure-rest-api",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A CDK construct for creating API Gateway REST APIs secured with API Key authentication and usage plan throttling",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/aligent/cdk-constructs/tree/main/packages/constructs/secure-rest-api#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/aligent/cdk-constructs.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/aligent/cdk-constructs/issues"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "npx nx test secure-rest-api",
|
|
19
|
+
"lint": "npx nx lint secure-rest-api"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/jest": "^29.5.10",
|
|
23
|
+
"@types/node": "^20.19.39",
|
|
24
|
+
"aws-cdk": "^2.1120.0",
|
|
25
|
+
"jest": "^29.7.0",
|
|
26
|
+
"ts-jest": "^29.4.9",
|
|
27
|
+
"ts-node": "^10.9.1",
|
|
28
|
+
"typescript": "^5.3.2"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"source-map-support": "^0.5.21"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"aws-cdk-lib": "^2.113.0",
|
|
35
|
+
"constructs": "^10.5.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "secure-rest-api",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/constructs/secure-rest-api/lib",
|
|
5
|
+
"projectType": "application",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "nx:run-commands",
|
|
9
|
+
"options": {
|
|
10
|
+
"command": "tsc --project tsconfig.app.json",
|
|
11
|
+
"cwd": "packages/constructs/secure-rest-api"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"lint": {
|
|
15
|
+
"executor": "@nx/eslint:lint",
|
|
16
|
+
"outputs": ["{options.outputFile}"]
|
|
17
|
+
},
|
|
18
|
+
"test": {
|
|
19
|
+
"executor": "@nx/jest:jest",
|
|
20
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
21
|
+
"options": {
|
|
22
|
+
"jestConfig": "packages/constructs/secure-rest-api/jest.config.ts",
|
|
23
|
+
"passWithNoTests": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publish": {
|
|
27
|
+
"executor": "nx:run-commands",
|
|
28
|
+
"options": {
|
|
29
|
+
"command": "npm publish --access public",
|
|
30
|
+
"cwd": "packages/constructs/secure-rest-api"
|
|
31
|
+
},
|
|
32
|
+
"dependsOn": ["build"]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"tags": []
|
|
36
|
+
}
|
package/tsconfig.json
ADDED