@kensio/yulin 1.20.15 → 1.20.16

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.
Files changed (95) hide show
  1. package/README.md +13 -0
  2. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  3. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  4. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  5. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  6. package/dist/sdk/send-patch.d.ts +8 -0
  7. package/dist/sdk/send-patch.js +11 -0
  8. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  9. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  10. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  11. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  12. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  13. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  14. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  15. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  17. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  18. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  19. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  20. package/dist/service/aws/sim-aws.js +2 -1
  21. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  22. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  23. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  25. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  26. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  27. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  28. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  29. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  30. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  31. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  32. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  33. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  34. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  35. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  36. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  37. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  38. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  39. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  40. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  41. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  42. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  43. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  44. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  45. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  46. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  47. package/dist/service/sts/sim-sts.d.ts +9 -1
  48. package/dist/service/sts/sim-sts.js +4 -0
  49. package/docs/README.md +54 -0
  50. package/docs/ai-skill/README.md +69 -0
  51. package/docs/cli/README.md +284 -0
  52. package/docs/factories/README.md +140 -0
  53. package/docs/lint/README.md +176 -0
  54. package/docs/non-aws-dependencies/README.md +261 -0
  55. package/docs/sdk/README.md +272 -0
  56. package/docs/serve/README.md +1126 -0
  57. package/docs/services/acm/README.md +823 -0
  58. package/docs/services/apigateway/README.md +1919 -0
  59. package/docs/services/apigatewayv2/README.md +2678 -0
  60. package/docs/services/athena/README.md +1044 -0
  61. package/docs/services/bedrock/README.md +350 -0
  62. package/docs/services/cloudformation/README.md +3440 -0
  63. package/docs/services/cloudfront/README.md +2874 -0
  64. package/docs/services/cloudwatch/README.md +420 -0
  65. package/docs/services/cognito/README.md +4923 -0
  66. package/docs/services/dynamodb/README.md +3784 -0
  67. package/docs/services/ecr/README.md +302 -0
  68. package/docs/services/ecs/README.md +2010 -0
  69. package/docs/services/elbv2/README.md +1897 -0
  70. package/docs/services/eventbridge/README.md +1110 -0
  71. package/docs/services/firehose/README.md +805 -0
  72. package/docs/services/glue/README.md +501 -0
  73. package/docs/services/iam/README.md +1283 -0
  74. package/docs/services/kinesis/README.md +454 -0
  75. package/docs/services/kms/README.md +659 -0
  76. package/docs/services/lambda/README.md +3645 -0
  77. package/docs/services/logs/README.md +868 -0
  78. package/docs/services/organizations/README.md +574 -0
  79. package/docs/services/personalize/README.md +835 -0
  80. package/docs/services/rekognition/README.md +1070 -0
  81. package/docs/services/route53/README.md +1783 -0
  82. package/docs/services/s3/README.md +3024 -0
  83. package/docs/services/scheduler/README.md +806 -0
  84. package/docs/services/secretsmanager/README.md +667 -0
  85. package/docs/services/ses/README.md +1011 -0
  86. package/docs/services/sns/README.md +1904 -0
  87. package/docs/services/sqs/README.md +1185 -0
  88. package/docs/services/ssm/README.md +1072 -0
  89. package/docs/services/stepfunctions/README.md +1332 -0
  90. package/docs/services/sts/README.md +245 -0
  91. package/docs/services/wafv2/README.md +1378 -0
  92. package/docs/terraform/README.md +248 -0
  93. package/docs/time/README.md +284 -0
  94. package/llms.txt +59 -0
  95. package/package.json +7 -4
@@ -0,0 +1,1919 @@
1
+ # Simulated API Gateway REST APIs
2
+
3
+ Yulin includes a simulated API Gateway v1 service, reachable as `simAws.apiGateway()`. It covers the
4
+ REST API resource tree, the methods declared on it, a Lambda proxy integration behind each method,
5
+ and the deployments and stages that publish them. REST-API-specific types are imported from the
6
+ `@kensio/yulin/apigateway` subpath.
7
+
8
+ This is the v1 service. HTTP APIs are v2, on a separate SDK client, and they are documented under
9
+ [API Gateway HTTP APIs](https://yulinsim.dev/services/apigatewayv2/). The two hold separate state. A REST API created here
10
+ stays out of `simAws.apiGatewayV2()`.
11
+
12
+ A handler behind a REST API can be tested against a real HTTP request, with no hand-built event to
13
+ keep in step.
14
+
15
+ ## Creating a REST API
16
+
17
+ `CreateRestApiCommand` creates a REST API together with the root resource every path hangs off.
18
+
19
+ ```typescript sim-apigateway-create-rest-api
20
+ /**
21
+ * Creating a simulated API Gateway REST API.
22
+ *
23
+ * The root resource is created with the API, and `rootResourceId` is what the
24
+ * first `CreateResource` names as its parent.
25
+ */
26
+
27
+ import { CreateRestApiCommand } from "@aws-sdk/client-api-gateway";
28
+
29
+ import { SimAws } from "@kensio/yulin";
30
+
31
+ const simAws = new SimAws();
32
+ const apiGateway = simAws
33
+ .account("555555555555")
34
+ .region("eu-west-2")
35
+ .apiGateway();
36
+
37
+ const created = await apiGateway.createRestApi(
38
+ new CreateRestApiCommand({ name: "orders", description: "The orders API" }),
39
+ );
40
+
41
+ console.log(created.name);
42
+ // "orders"
43
+
44
+ console.log(typeof created.id);
45
+ // "string"
46
+
47
+ console.log(typeof created.rootResourceId);
48
+ // "string"
49
+ ```
50
+
51
+ A REST API name identifies nothing. Two APIs in one account and region may share a name, and the id
52
+ is what tells them apart. Hold the id the create returns.
53
+
54
+ ## Building the path tree
55
+
56
+ A REST API path is a chain of resources, each holding one segment. `CreateResourceCommand` adds a
57
+ segment under a parent and reports the full path its place in the tree gives it.
58
+
59
+ ```typescript sim-apigateway-resource-tree
60
+ /**
61
+ * Building /orders/{orderId} out of two resources.
62
+ *
63
+ * Each resource holds one segment and names its parent, and API Gateway
64
+ * computes the full path from where the resource sits.
65
+ */
66
+
67
+ import {
68
+ CreateResourceCommand,
69
+ CreateRestApiCommand,
70
+ GetResourcesCommand,
71
+ } from "@aws-sdk/client-api-gateway";
72
+
73
+ import { SimAws } from "@kensio/yulin";
74
+
75
+ const simAws = new SimAws();
76
+ const apiGateway = simAws.apiGateway();
77
+
78
+ const api = await apiGateway.createRestApi(
79
+ new CreateRestApiCommand({ name: "orders" }),
80
+ );
81
+
82
+ const orders = await apiGateway.createResource(
83
+ new CreateResourceCommand({
84
+ restApiId: api.id,
85
+ parentId: api.rootResourceId,
86
+ pathPart: "orders",
87
+ }),
88
+ );
89
+
90
+ await apiGateway.createResource(
91
+ new CreateResourceCommand({
92
+ restApiId: api.id,
93
+ parentId: orders.id,
94
+ pathPart: "{orderId}",
95
+ }),
96
+ );
97
+
98
+ const listed = await apiGateway.getResources(
99
+ new GetResourcesCommand({ restApiId: api.id }),
100
+ );
101
+
102
+ console.log(listed.items.map((resource) => resource.path));
103
+ // [ "/", "/orders", "/orders/{orderId}" ]
104
+ ```
105
+
106
+ A segment is a literal such as `orders`, a path parameter such as `{orderId}`, or a greedy path
107
+ parameter such as `{proxy+}`. A greedy segment matches the rest of the request path. A resource
108
+ holding one therefore takes no children, and adding under it is refused.
109
+
110
+ Deleting a resource deletes everything under it, the way real API Gateway does. The root resource
111
+ stays, because every REST API has one.
112
+
113
+ ## Methods and their integrations
114
+
115
+ A method is declared on a resource with `PutMethodCommand`, and what it does with a request goes
116
+ behind it with `PutIntegrationCommand`. Both address the same resource id and HTTP method, since a
117
+ REST API method has no id of its own.
118
+
119
+ ```typescript sim-apigateway-method-integration
120
+ /**
121
+ * Declaring an ANY method on a greedy resource and putting a Lambda proxy
122
+ * integration behind it, which is the shape a CDK LambdaRestApi produces.
123
+ */
124
+
125
+ import {
126
+ CreateResourceCommand,
127
+ CreateRestApiCommand,
128
+ GetMethodCommand,
129
+ PutIntegrationCommand,
130
+ PutMethodCommand,
131
+ } from "@aws-sdk/client-api-gateway";
132
+
133
+ import { SimAws } from "@kensio/yulin";
134
+
135
+ const simAws = new SimAws();
136
+ const apiGateway = simAws.apiGateway();
137
+
138
+ const api = await apiGateway.createRestApi(
139
+ new CreateRestApiCommand({ name: "orders" }),
140
+ );
141
+ const proxy = await apiGateway.createResource(
142
+ new CreateResourceCommand({
143
+ restApiId: api.id,
144
+ parentId: api.rootResourceId,
145
+ pathPart: "{proxy+}",
146
+ }),
147
+ );
148
+
149
+ await apiGateway.putMethod(
150
+ new PutMethodCommand({
151
+ restApiId: api.id,
152
+ resourceId: proxy.id,
153
+ httpMethod: "ANY",
154
+ authorizationType: "NONE",
155
+ }),
156
+ );
157
+
158
+ await apiGateway.putIntegration(
159
+ new PutIntegrationCommand({
160
+ restApiId: api.id,
161
+ resourceId: proxy.id,
162
+ httpMethod: "ANY",
163
+ type: "AWS_PROXY",
164
+ // API Gateway always calls a Lambda integration with POST, whatever
165
+ // method the client used.
166
+ integrationHttpMethod: "POST",
167
+ uri:
168
+ "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/" +
169
+ "arn:aws:lambda:eu-west-2:111111111111:function:orders/invocations",
170
+ }),
171
+ );
172
+
173
+ const method = await apiGateway.getMethod(
174
+ new GetMethodCommand({
175
+ restApiId: api.id,
176
+ resourceId: proxy.id,
177
+ httpMethod: "ANY",
178
+ }),
179
+ );
180
+
181
+ console.log(method.methodIntegration?.uri);
182
+ // "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/
183
+ // arn:aws:lambda:eu-west-2:111111111111:function:orders/invocations",
184
+ // echoed back as one line, the way it was configured
185
+ ```
186
+
187
+ The integration URI is written either as the bare function ARN, which CDK emits, or wrapped in the
188
+ API Gateway invoke path above, which CloudFormation templates and OpenAPI documents emit. Both reach
189
+ the same function, and the string is echoed back as it was configured, the way real API Gateway does.
190
+ A version or alias qualifier on the end of the ARN is kept. An integration built on an alias
191
+ therefore follows that alias.
192
+
193
+ Deleting a method deletes its integration, because a REST API integration is part of the method.
194
+
195
+ ## Deployments and stages
196
+
197
+ A REST API has an invocation URL once a stage exists, and every stage is the first path segment of
198
+ that URL. An HTTP API can serve a `$default` stage at the root, and a REST API always carries the
199
+ segment.
200
+
201
+ ```typescript sim-apigateway-deploy-stage
202
+ /**
203
+ * Publishing an API to a stage, and building the URL a request to it goes to.
204
+ *
205
+ * `CreateDeployment` with a `stageName` is the one-call form. Without it the
206
+ * deployment is created and a `CreateStage` points at it separately. Deploying
207
+ * again to a stage that is already there points that stage at the new
208
+ * deployment, which is what every release after the first does.
209
+ */
210
+
211
+ import {
212
+ CreateDeploymentCommand,
213
+ CreateRestApiCommand,
214
+ GetStageCommand,
215
+ } from "@aws-sdk/client-api-gateway";
216
+
217
+ import { SimAws } from "@kensio/yulin";
218
+
219
+ const simAws = new SimAws();
220
+ const apiGateway = simAws
221
+ .account("555555555555")
222
+ .region("eu-west-2")
223
+ .apiGateway();
224
+
225
+ const api = await apiGateway.createRestApi(
226
+ new CreateRestApiCommand({ name: "orders" }),
227
+ );
228
+
229
+ const deployment = await apiGateway.createDeployment(
230
+ new CreateDeploymentCommand({
231
+ restApiId: api.id,
232
+ stageName: "prod",
233
+ variables: { catalogue: "v2" },
234
+ }),
235
+ );
236
+
237
+ const stage = await apiGateway.getStage(
238
+ new GetStageCommand({ restApiId: api.id, stageName: "prod" }),
239
+ );
240
+
241
+ console.log(stage.deploymentId === deployment.id);
242
+ // true
243
+
244
+ const restApi = apiGateway.findRestApi(api.id);
245
+ console.log(restApi?.invokeUrl("prod"));
246
+ // "https://<api-id>.execute-api.eu-west-2.amazonaws.com/prod"
247
+ ```
248
+
249
+ `invokeUrl` is a simulator accessor. Real API Gateway reports no endpoint for a REST API and leaves
250
+ callers to build the URL themselves. CDK's `RestApi.urlForPath` builds the same one.
251
+
252
+ Real API Gateway freezes the resources and methods into a deployment, and an edit made afterwards
253
+ reaches no client until another deployment is created. Here a stage serves the API's current
254
+ resources. A test that edits a method sees the change straight away, with no redeployment in
255
+ between. That is the one place this departs from AWS.
256
+
257
+ ## Serving a request
258
+
259
+ A request to the stage's invoke URL walks the resource tree to a method and invokes that method's
260
+ integration, and the handler's response becomes the HTTP response.
261
+
262
+ ```typescript sim-apigateway-serve
263
+ /**
264
+ * Serving a request through a REST API to its Lambda proxy integration.
265
+ *
266
+ * The handler reads the payload format 1.0 event a REST API sends, which is
267
+ * the older of the two formats and the only one a REST API uses.
268
+ */
269
+
270
+ import { SimAws } from "@kensio/yulin";
271
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
272
+ import { serveSimAws } from "@kensio/yulin/serve";
273
+
274
+ const simAws = new SimAws();
275
+
276
+ const restApi = await simRestApiLambdaProxyFactory.make(
277
+ {
278
+ resourcePaths: ["/orders/{orderId}"],
279
+ handler: (event) => ({
280
+ statusCode: 200,
281
+ headers: { "content-type": "text/plain" },
282
+ body: `order ${event.pathParameters?.["orderId"] ?? "none"}`,
283
+ }),
284
+ },
285
+ simAws,
286
+ );
287
+
288
+ const srv = await serveSimAws({ simAws });
289
+
290
+ const response = await fetch(
291
+ srv.localUrl(`${restApi.invokeUrl("prod")}/orders/6`),
292
+ );
293
+
294
+ console.log(response.status);
295
+ // 200
296
+
297
+ console.log(await response.text());
298
+ // "order 6"
299
+
300
+ await srv.close();
301
+ ```
302
+
303
+ `simRestApiLambdaProxyFactory` builds the function, the resources, the method, the integration, the
304
+ invoke permission and the deployment in one call. A test about serving wants all of them and is
305
+ about none of them. A test about the commands themselves sends them one at a time.
306
+
307
+ ### The event a handler receives
308
+
309
+ A REST API sends payload format 1.0. It carries both a single-value and a multi-value map for the
310
+ headers and the query string, and it sends `null` for an empty map where format 2.0 omits the field:
311
+
312
+ | Field | Empty case |
313
+ | ---------------------------------------------------------- | ---------- |
314
+ | `queryStringParameters`, `multiValueQueryStringParameters` | `null` |
315
+ | `pathParameters`, `stageVariables` | `null` |
316
+ | `body` | `null` |
317
+
318
+ `resource` is the template the path matched, such as `/orders/{orderId}`, and `path` is the path the
319
+ client asked for, stage segment and all. A handler behind a `{proxy+}` reads `resource` to tell which
320
+ template caught its request.
321
+
322
+ Each format's handler reads the other format's event wrongly. One function behind both an HTTP API
323
+ and a REST API therefore has to pick a side.
324
+
325
+ ### The response a handler returns
326
+
327
+ A REST API proxy integration takes one shape. A result carrying a numeric `statusCode` becomes the
328
+ response, and `multiValueHeaders` sends a header more than once. Anything else is a 502 with
329
+ `Internal server error`. That is what real API Gateway answers when it cannot read the integration
330
+ response. Payload format 2.0 is the lenient one, wrapping an unrecognised value in a 200, and a
331
+ handler relying on that behaves differently here for the same reason it does on AWS.
332
+
333
+ ### Answers when the request matches nothing
334
+
335
+ | Case | Answer |
336
+ | --------------------------------------------- | ---------------------------------- |
337
+ | A stage the API does not serve | 403 `Forbidden` |
338
+ | A path or method the stage has no entry for | 403 `Missing Authentication Token` |
339
+ | The generated endpoint switched off | 403 `Forbidden` |
340
+ | No integration, no function, or no permission | 502 `Internal server error` |
341
+ | The handler threw | 502 `Internal server error` |
342
+
343
+ `Missing Authentication Token` is the wording real API Gateway is well known for. It answers a path
344
+ that matched nothing just as much as one that needed credentials.
345
+
346
+ ### The invoke permission
347
+
348
+ A method's integration runs once the function's resource policy allows
349
+ `apigateway.amazonaws.com` to invoke it, exactly as on AWS. The method the request matched is
350
+ supplied as `AWS:SourceArn`, in the form
351
+ `arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{METHOD}/{resourcePath}`. A permission
352
+ granted for one method therefore leaves the others closed. CDK wildcards the stage, method and path segments,
353
+ which admits every method of the API.
354
+
355
+ ## Throttling a stage and a method
356
+
357
+ A stage holds a token bucket for every method it serves. A method setting is addressed by
358
+ `{resourcePath}/{httpMethod}`, which is how API Gateway addresses one, and the entry keyed with a
359
+ resource path of `/*` and a method of `*` is the stage default. `throttlingRateLimit` is requests per
360
+ second, and `throttlingBurstLimit` is how many requests a method will take at once.
361
+
362
+ A request that finds an empty bucket is answered 429 with `{"message":"Too Many Requests"}`. The
363
+ method's authorizer and its integration are both skipped.
364
+
365
+ The buckets refill against the simulated clock. Freeze it, spend a method's burst, assert on the 429,
366
+ then move a second on and watch the method serve again.
367
+
368
+ ```typescript sim-apigateway-throttling
369
+ /**
370
+ * Throttling a REST API stage and one of its methods.
371
+ */
372
+
373
+ import { SimAws } from "@kensio/yulin";
374
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
375
+ import { serveSimAws } from "@kensio/yulin/serve";
376
+
377
+ const simAws = new SimAws();
378
+
379
+ const restApi = await simRestApiLambdaProxyFactory.make(
380
+ {
381
+ resourcePaths: ["/password-reset", "/profile"],
382
+ httpMethod: "POST",
383
+ methodSettings: {
384
+ "/*/*": { throttlingRateLimit: 10, throttlingBurstLimit: 5 },
385
+ "/password-reset/POST": {
386
+ throttlingRateLimit: 1,
387
+ throttlingBurstLimit: 2,
388
+ },
389
+ },
390
+ handler: () => ({ statusCode: 200, body: "ok" }),
391
+ },
392
+ simAws,
393
+ );
394
+
395
+ const srv = await serveSimAws({ simAws });
396
+
397
+ // Stop simulated time. A bucket now refills only when this example moves it.
398
+ simAws.clock().freeze();
399
+
400
+ const passwordReset = async (): Promise<Response> =>
401
+ await fetch(srv.localUrl(`${restApi.invokeUrl("prod")}/password-reset`), {
402
+ method: "POST",
403
+ });
404
+
405
+ const first = await passwordReset();
406
+ const second = await passwordReset();
407
+ const third = await passwordReset();
408
+
409
+ console.log(first.status, second.status, third.status);
410
+ console.log(await third.text());
411
+
412
+ // Another method, drawing on the stage default and a bucket of its own.
413
+ const profile = await fetch(
414
+ srv.localUrl(`${restApi.invokeUrl("prod")}/profile`),
415
+ { method: "POST" },
416
+ );
417
+ console.log(profile.status);
418
+
419
+ // One second at a rate limit of one is one token back.
420
+ await simAws.clock().advanceBy({ seconds: 1 });
421
+ const afterASecond = await passwordReset();
422
+ console.log(afterASecond.status);
423
+
424
+ await srv.close();
425
+ ```
426
+
427
+ The burst of two is served, the third password reset is refused, and the profile method is untouched
428
+ by any of it:
429
+
430
+ ```text
431
+ 200 200 429
432
+ {"message":"Too Many Requests"}
433
+ 200
434
+ 200
435
+ ```
436
+
437
+ A template writes the same thing as a list, with each entry naming the method it applies to:
438
+
439
+ ```yaml
440
+ ProdStage:
441
+ Type: AWS::ApiGateway::Stage
442
+ Properties:
443
+ RestApiId: !Ref Api
444
+ DeploymentId: !Ref Deployment
445
+ StageName: prod
446
+ MethodSettings:
447
+ - ResourcePath: "/*"
448
+ HttpMethod: "*"
449
+ ThrottlingRateLimit: 10
450
+ ThrottlingBurstLimit: 5
451
+ - ResourcePath: "/password-reset"
452
+ HttpMethod: POST
453
+ ThrottlingRateLimit: 1
454
+ ThrottlingBurstLimit: 2
455
+ ```
456
+
457
+ Every client of a method draws on the same bucket. Two callers sending one request each spend two
458
+ tokens between them. A WAFv2 `RateBasedStatement` counts each client on its own (see
459
+ [Rate limiting](https://yulinsim.dev/services/wafv2/#rate-limiting)), and a stack often carries both.
460
+
461
+ A method is throttled here only where the settings reaching it name both limits. Naming one alone
462
+ leaves the other at the account limit on real AWS. Account limits are outside this simulation, and a
463
+ method configured that way is served unthrottled.
464
+
465
+ The `httpMethod` half of a key is the one the method was declared with. A resource declaring `ANY`
466
+ is named `ANY`, whatever method the client sent.
467
+
468
+ Real `CreateStage` carries no method settings. AWS sets them with `UpdateStage` patch operations,
469
+ which are outside this simulation, or from an `AWS::ApiGateway::Stage`. The `methodSettings` input
470
+ above is this simulator's own, so that a test can throttle a stage without a template, and the SDK's
471
+ `CreateStageCommand` declares no such member. `GetStage` reports the settings the way AWS reports
472
+ them.
473
+
474
+ ## Authorizing a method
475
+
476
+ A method is open unless it names an authorizer. `CreateAuthorizerCommand` creates one, and
477
+ `PutMethodCommand` binds it to a method with `authorizationType: "CUSTOM"` and the `authorizerId` the
478
+ API allocated.
479
+
480
+ A `TOKEN` authorizer reads one header and sends its value to a Lambda function of its own. That
481
+ function answers an IAM policy document, evaluated for `execute-api:Invoke` against the ARN of the
482
+ request being made. Whatever `context` it returns reaches the handler.
483
+
484
+ A `REQUEST` authorizer sends the whole request to its function (see
485
+ [A REQUEST authorizer](#a-request-authorizer)), so it can identify a caller by several headers
486
+ together or by the query string. It answers the same policy document.
487
+
488
+ ```typescript sim-apigateway-token-authorizer
489
+ /**
490
+ * Gating a REST API method with a TOKEN Lambda authorizer.
491
+ *
492
+ * The authorizer reads the Authorization header, and the policy it answers is
493
+ * evaluated against the ARN of the request being made.
494
+ */
495
+
496
+ import { SimAws } from "@kensio/yulin";
497
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
498
+ import { serveSimAws } from "@kensio/yulin/serve";
499
+
500
+ const simAws = new SimAws();
501
+
502
+ const restApi = await simRestApiLambdaProxyFactory.make(
503
+ {
504
+ resourcePaths: ["/orders"],
505
+ authorizerHandler: (event) => ({
506
+ principalId: "user-6",
507
+ context: { tenantId: "acme" },
508
+ policyDocument: {
509
+ Version: "2012-10-17",
510
+ Statement: [
511
+ {
512
+ Action: "execute-api:Invoke",
513
+ Effect:
514
+ event.authorizationToken === "Bearer valid" ? "Allow" : "Deny",
515
+ Resource: event.methodArn,
516
+ },
517
+ ],
518
+ },
519
+ }),
520
+ handler: (event) => ({
521
+ statusCode: 200,
522
+ headers: { "content-type": "application/json" },
523
+ body: JSON.stringify(event.requestContext.authorizer),
524
+ }),
525
+ },
526
+ simAws,
527
+ );
528
+
529
+ const srv = await serveSimAws({ simAws });
530
+ const url = srv.localUrl(`${restApi.invokeUrl("prod")}/orders`);
531
+
532
+ const admitted = await fetch(url, {
533
+ headers: { authorization: "Bearer valid" },
534
+ });
535
+
536
+ console.log(admitted.status);
537
+ // 200
538
+
539
+ console.log(await admitted.text());
540
+ // '{"tenantId":"acme","principalId":"user-6"}'
541
+
542
+ const refused = await fetch(url, {
543
+ headers: { authorization: "Bearer stale" },
544
+ });
545
+
546
+ console.log(refused.status);
547
+ // 403
548
+
549
+ const anonymous = await fetch(url);
550
+
551
+ console.log(anonymous.status);
552
+ // 401
553
+
554
+ await srv.close();
555
+ ```
556
+
557
+ `simRestApiLambdaProxyFactory` builds the authorizer's function, the authorizer, its invoke
558
+ permission and the methods bound to it when it is given an `authorizerHandler`.
559
+
560
+ ### The event the authorizer receives
561
+
562
+ A `TOKEN` authorizer sees three fields and no more.
563
+
564
+ | Field | What it carries |
565
+ | -------------------- | ------------------------------------------------------------------------ |
566
+ | `type` | The literal `TOKEN` |
567
+ | `authorizationToken` | The value the request carried at the identity source |
568
+ | `methodArn` | `arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{METHOD}/{path}` |
569
+
570
+ The `methodArn` names the path the client asked for rather than the resource template it matched. A
571
+ request to `/orders/6` behind an `/orders/{orderId}` resource is named as `GET/orders/6`.
572
+
573
+ A `TOKEN` authorizer's identity source is one header, written as
574
+ `method.request.header.Authorization`. An expression naming anywhere else is refused by
575
+ `CreateAuthorizer`, because an authorizer that looks where the request never carries anything
576
+ refuses everyone. That refusal reads like a signing problem when the configuration is what went
577
+ wrong.
578
+
579
+ ### A REQUEST authorizer
580
+
581
+ `type: "REQUEST"` sends the request itself to the function. The `identitySource` is one
582
+ comma-separated string naming as many places as identify a caller, in the
583
+ `method.request.header.<name>` and `method.request.querystring.<name>` forms. An HTTP API takes a
584
+ list here, and a REST API takes the string.
585
+
586
+ ```typescript sim-apigateway-request-authorizer
587
+ /**
588
+ * Gating a REST API method with a REQUEST Lambda authorizer.
589
+ *
590
+ * The authorizer reads a header and a query string parameter together, which
591
+ * is what a TOKEN authorizer cannot do.
592
+ */
593
+
594
+ import { SimAws } from "@kensio/yulin";
595
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
596
+ import { serveSimAws } from "@kensio/yulin/serve";
597
+
598
+ const simAws = new SimAws();
599
+
600
+ const restApi = await simRestApiLambdaProxyFactory.make(
601
+ {
602
+ resourcePaths: ["/orders"],
603
+ httpMethod: "GET",
604
+ authorizerIdentitySource:
605
+ "method.request.header.X-Tenant,method.request.querystring.plan",
606
+ requestAuthorizerHandler: (event) => ({
607
+ principalId: event.headers["x-tenant"],
608
+ context: { plan: event.queryStringParameters["plan"] },
609
+ policyDocument: {
610
+ Version: "2012-10-17",
611
+ Statement: [
612
+ {
613
+ Action: "execute-api:Invoke",
614
+ Effect:
615
+ event.queryStringParameters["plan"] === "gold" ? "Allow" : "Deny",
616
+ Resource: event.methodArn,
617
+ },
618
+ ],
619
+ },
620
+ }),
621
+ handler: (event) => ({
622
+ statusCode: 200,
623
+ headers: { "content-type": "application/json" },
624
+ body: JSON.stringify(event.requestContext.authorizer),
625
+ }),
626
+ },
627
+ simAws,
628
+ );
629
+
630
+ const srv = await serveSimAws({ simAws });
631
+ const url = srv.localUrl(`${restApi.invokeUrl("prod")}/orders`).href;
632
+ const headers = { "x-tenant": "acme" };
633
+
634
+ const admitted = await fetch(`${url}?plan=gold`, { headers });
635
+
636
+ console.log(admitted.status);
637
+ // 200
638
+
639
+ console.log(await admitted.text());
640
+ // '{"plan":"gold","principalId":"acme"}'
641
+
642
+ const refused = await fetch(`${url}?plan=free`, { headers });
643
+
644
+ console.log(refused.status);
645
+ // 403
646
+
647
+ const anonymous = await fetch(`${url}?plan=gold`);
648
+
649
+ console.log(anonymous.status);
650
+ // 401
651
+
652
+ await srv.close();
653
+ ```
654
+
655
+ The function is invoked only once the request carries something at every identity source. The
656
+ request with no `X-Tenant` header above got its 401 without the function running.
657
+
658
+ An authorizer created with no `identitySource` is refused. Real AWS invokes that authorizer for
659
+ every request including one carrying nothing, and CDK's `RequestAuthorizer` requires at least one
660
+ source.
661
+
662
+ #### The event a REQUEST authorizer receives
663
+
664
+ The event is the payload format 1.0 request event with `type` and `methodArn` added and the body
665
+ left out.
666
+
667
+ | Field | What it carries |
668
+ | ---------------------------------------------------------- | -------------------------------------------------------------- |
669
+ | `type` | The literal `REQUEST` |
670
+ | `methodArn` | The ARN of the request, the same one a `TOKEN` authorizer gets |
671
+ | `resource`, `path`, `httpMethod` | The resource template, the path asked for and the method |
672
+ | `headers`, `multiValueHeaders` | The request headers, in both forms payload format 1.0 sends |
673
+ | `queryStringParameters`, `multiValueQueryStringParameters` | The query string, in both forms |
674
+ | `pathParameters`, `stageVariables` | What the resource path captured, and the stage's variables |
675
+ | `requestContext` | The same block a handler gets, without the `authorizer` member |
676
+
677
+ The maps are empty objects where the request supplied nothing. An integration event sends `null`
678
+ there, and AWS's own example of the authorizer event sends `{}`, so a function reading
679
+ `event.queryStringParameters.plan` finds nothing rather than throwing.
680
+
681
+ ### Answering with a policy
682
+
683
+ A REST API authorizer always answers a policy. An HTTP API authorizer may answer a boolean instead.
684
+ A function written for one is read wrongly by the other.
685
+
686
+ ```json
687
+ {
688
+ "principalId": "user-6",
689
+ "context": { "tenantId": "acme" },
690
+ "policyDocument": {
691
+ "Version": "2012-10-17",
692
+ "Statement": [
693
+ {
694
+ "Action": "execute-api:Invoke",
695
+ "Effect": "Allow",
696
+ "Resource": "arn:aws:execute-api:eu-west-2:111111111111:a1b2c3d4e5/prod/GET/orders"
697
+ }
698
+ ]
699
+ }
700
+ }
701
+ ```
702
+
703
+ The document goes to simulated IAM and is evaluated for `execute-api:Invoke` on the `methodArn`. A
704
+ policy naming one method leaves the others unauthorized, and an authorizer wanting to open the whole
705
+ API wildcards the resource the way any IAM policy does.
706
+
707
+ `principalId` is a name the authorizer chose for the caller. It identifies that caller in the
708
+ authorizer's own logs, and IAM never sees it.
709
+
710
+ ### The context the handler receives
711
+
712
+ `context` reaches the handler under `requestContext.authorizer`, flattened alongside `principalId`.
713
+ Payload format 2.0 keeps the context in a block of its own, so a handler moved between a REST API and
714
+ an HTTP API reads a different shape.
715
+
716
+ An open method has no caller to describe, and leaves `requestContext.authorizer` out of the event
717
+ altogether.
718
+
719
+ ### What a refused request gets back
720
+
721
+ | Case | Answer |
722
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
723
+ | No value at the identity source | 401 `Unauthorized` |
724
+ | The authorizer returned `{ "errorMessage": "Unauthorized" }` | 401 `Unauthorized` |
725
+ | A Deny statement matched the method | 403 `User is not authorized to access this resource with an explicit deny` |
726
+ | The policy allowed nothing covering the method | 403 `User is not authorized to access this resource` |
727
+ | The authorizer failed, or answered a shape AWS cannot read | 500 `Internal server error` |
728
+
729
+ A request carrying nothing at the identity source is refused before the function is invoked. An
730
+ authorizer counting its own invocations never sees one.
731
+
732
+ The 401 body member is `message` and the 403 one is `Message`. Real API Gateway is inconsistent about
733
+ the two and this follows it.
734
+
735
+ A function that throws is an authorizer failure, and gets the 500. Real Lambda turns a thrown error
736
+ into a payload carrying `errorMessage`, while simulated Lambda rejects with the error itself.
737
+ Returning the value is the way to ask for a 401.
738
+
739
+ ### The authorizer's invoke permission
740
+
741
+ The authorizer's function needs a grant of its own, under an ARN naming the authorizer:
742
+
743
+ ```text
744
+ arn:aws:execute-api:{region}:{account}:{apiId}/authorizers/{authorizerId}
745
+ ```
746
+
747
+ That ARN names no stage. A function used both as an integration and as an authorizer needs two
748
+ permissions, as it does on AWS. CDK's `TokenAuthorizer` and `RequestAuthorizer` both write this
749
+ one.
750
+
751
+ ### Caching the authorizer's decision
752
+
753
+ `authorizerResultTtlInSeconds` holds a decision for that many seconds. A second request presenting
754
+ the same identity within that period reaches the handler without the function running again. AWS
755
+ accepts a whole number of seconds up to 3600, and 0 switches the holding off.
756
+
757
+ An authorizer that says nothing about the member gets 0 here and gets 300 on real API Gateway.
758
+ Write the member out to have a test and a deployment agree on it. CDK writes it either way, at five
759
+ minutes by default (see [CDK](#cdk)).
760
+
761
+ A `TOKEN` authorizer is keyed on the token it was handed. A `REQUEST` authorizer is keyed on the
762
+ values its identity sources found, in the order they were configured. Both are held per method,
763
+ because what is held is the admission or refusal the authorizer's policy produced for one method
764
+ ARN, and that answer covers no other method. (An HTTP API keys on the identity source values alone,
765
+ and one decision there covers every route using the authorizer.)
766
+
767
+ A refusal is held the way an admission is. AWS holds whatever answer the authorizer gave. A failed
768
+ authorizer holds no answer, and its function is asked again on the next request.
769
+
770
+ Expiry follows simulated time. `simAws.clock().advanceBy(...)` drops a decision that was being
771
+ reused a moment before.
772
+
773
+ ```typescript sim-apigateway-authorizer-cache
774
+ /**
775
+ * Caching a simulated REST API Lambda authorizer's decision.
776
+ *
777
+ * The authorizer counts its own invocations and reports the count in its
778
+ * context, so the handler shows which decision served each request.
779
+ */
780
+
781
+ import { SimAws } from "@kensio/yulin";
782
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
783
+ import { serveSimAws } from "@kensio/yulin/serve";
784
+
785
+ const simAws = new SimAws();
786
+ const counter = { invocations: 0 };
787
+
788
+ const restApi = await simRestApiLambdaProxyFactory.make(
789
+ {
790
+ resourcePaths: ["/orders"],
791
+ authorizerResultTtlSeconds: 300,
792
+ authorizerHandler: (event) => {
793
+ counter.invocations += 1;
794
+
795
+ return {
796
+ principalId: "user-6",
797
+ context: { ...counter },
798
+ policyDocument: {
799
+ Version: "2012-10-17",
800
+ Statement: [
801
+ {
802
+ Action: "execute-api:Invoke",
803
+ Effect: "Allow",
804
+ Resource: event.methodArn,
805
+ },
806
+ ],
807
+ },
808
+ };
809
+ },
810
+ handler: (event) => ({
811
+ statusCode: 200,
812
+ headers: { "content-type": "application/json" },
813
+ body: JSON.stringify(event.requestContext.authorizer),
814
+ }),
815
+ },
816
+ simAws,
817
+ );
818
+
819
+ const srv = await serveSimAws({ simAws });
820
+ const url = srv.localUrl(`${restApi.invokeUrl("prod")}/orders`);
821
+ const call = async (): Promise<unknown> => {
822
+ const response = await fetch(url, {
823
+ headers: { authorization: "Bearer session-6" },
824
+ });
825
+
826
+ return await response.json();
827
+ };
828
+
829
+ console.log(await call());
830
+ // { invocations: 1, principalId: 'user-6' }
831
+
832
+ console.log(await call());
833
+ // { invocations: 1, principalId: 'user-6' }, held rather than asked again
834
+
835
+ await simAws.clock().advanceBy({ minutes: 6 });
836
+
837
+ console.log(await call());
838
+ // { invocations: 2, principalId: 'user-6' }
839
+
840
+ await srv.close();
841
+ ```
842
+
843
+ A `COGNITO_USER_POOLS` authorizer verifies each token as it arrives here, and `CreateAuthorizer`
844
+ refuses `authorizerResultTtlInSeconds` on one. Real API Gateway holds a Cognito authorizer's
845
+ decision too, and a token that expires inside the period is still accepted there.
846
+
847
+ ## Protecting a method with IAM
848
+
849
+ A method declared `authorizationType: "AWS_IAM"` reaches its integration only when the caller is
850
+ allowed `execute-api:Invoke` on the ARN of the method being called. IAM decides. The method takes no
851
+ authorizer, and naming one is refused by `PutMethod`.
852
+
853
+ The caller comes from the request, through either a SigV4 signature or an `x-sim-aws-caller` header
854
+ naming a principal directly. A request offering neither is anonymous, owns no policies, and is
855
+ refused. See [callers of HTTP requests](https://yulinsim.dev/services/iam/#callers-of-http-requests) in the IAM docs for how that
856
+ resolution works and how to sign a served request.
857
+
858
+ The ARN a request is authorized against is:
859
+
860
+ ```text
861
+ arn:aws:execute-api:<region>:<account>:<apiId>/<stage>/<METHOD>/<path>
862
+ ```
863
+
864
+ - The Account and Region are the API's own, not the caller's.
865
+ - The stage is the one that served the request.
866
+ - The method is the one the client sent, upper case. A `GET` reaching a resource declaring `ANY`
867
+ gives `GET`.
868
+ - The path is the request path with the stage segment and the leading slash taken off, so
869
+ `/prod/orders/42` served from stage `prod` gives `orders/42`. It is the path the client asked for,
870
+ because a policy names it by hand. A request to a method declared on `/orders/{orderId}` is
871
+ authorized under `GET/orders/42`, with no braces in it. A request to the API root gives an ARN
872
+ ending `/GET/`.
873
+
874
+ An identity policy may wildcard any part of that. `<apiId>/*`, `<apiId>/prod/*` and
875
+ `<apiId>/*/GET/orders/*` all allow a `GET` of `/orders/42` on stage `prod`.
876
+
877
+ ```typescript sim-apigateway-iam-authorization
878
+ /**
879
+ * Protecting a simulated REST API method with IAM.
880
+ *
881
+ * The caller the request was attributed to has to be allowed
882
+ * execute-api:Invoke on the method it is calling.
883
+ */
884
+
885
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
886
+
887
+ import { SimAws } from "@kensio/yulin";
888
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
889
+ import { serveSimAws } from "@kensio/yulin/serve";
890
+
891
+ const simAws = new SimAws();
892
+
893
+ const restApi = await simRestApiLambdaProxyFactory.make(
894
+ {
895
+ iamAuthorization: true,
896
+ resourcePaths: ["/orders/{orderId}"],
897
+ handler: (event) => ({
898
+ statusCode: 200,
899
+ headers: { "content-type": "text/plain" },
900
+ body: `orders for ${event.requestContext.identity.userArn ?? "nobody"}`,
901
+ }),
902
+ },
903
+ simAws,
904
+ );
905
+
906
+ // A Role of the API's own Account, allowed to call the orders methods of this
907
+ // API on the stage it is deployed to.
908
+ await simAws.iam().createRole(
909
+ new CreateRoleCommand({
910
+ RoleName: "Reporter",
911
+ AssumeRolePolicyDocument: JSON.stringify({
912
+ Version: "2012-10-17",
913
+ Statement: [
914
+ {
915
+ Effect: "Allow",
916
+ Principal: { AWS: "arn:aws:iam::888888888888:root" },
917
+ Action: "sts:AssumeRole",
918
+ },
919
+ ],
920
+ }),
921
+ }),
922
+ );
923
+
924
+ await simAws.iam().putRolePolicy(
925
+ new PutRolePolicyCommand({
926
+ RoleName: "Reporter",
927
+ PolicyName: "InvokeOrders",
928
+ PolicyDocument: JSON.stringify({
929
+ Version: "2012-10-17",
930
+ Statement: [
931
+ {
932
+ Effect: "Allow",
933
+ Action: "execute-api:Invoke",
934
+ Resource: `arn:aws:execute-api:us-east-1:888888888888:${restApi.apiId}/prod/GET/orders/*`,
935
+ },
936
+ ],
937
+ }),
938
+ }),
939
+ );
940
+
941
+ const srv = await serveSimAws({ simAws });
942
+ const url = srv.localUrl(`${restApi.invokeUrl("prod")}/orders/42`);
943
+
944
+ const anonymous = await fetch(url);
945
+
946
+ console.log(anonymous.status);
947
+ // 403
948
+
949
+ const reporter = await fetch(url, {
950
+ headers: { "x-sim-aws-caller": "arn:aws:iam::888888888888:role/Reporter" },
951
+ });
952
+
953
+ console.log(await reporter.text());
954
+ // "orders for arn:aws:iam::888888888888:role/Reporter"
955
+
956
+ await srv.close();
957
+ ```
958
+
959
+ A caller IAM does not allow gets 403 with `User is not authorized to access this resource`. An
960
+ explicit Deny gets that same body, where a Lambda authorizer's Deny gets one of its own.
961
+
962
+ Only the caller's identity policies are read. A REST API also has a resource policy on real AWS, and
963
+ `Policy` on an `AWS::ApiGateway::RestApi` is recorded against the Resource with the API deployed
964
+ without it. A caller from another Account is therefore always refused, because a cross-Account
965
+ request needs an Allow from each side. The way through, here as on AWS, is to assume a Role in the
966
+ API's Account.
967
+
968
+ ### The identity the handler receives
969
+
970
+ An admitted caller reaches the handler under `requestContext.identity`.
971
+
972
+ | Field | What it carries |
973
+ | ----------- | ---------------------------------- |
974
+ | `accountId` | The Account the caller's ARN names |
975
+ | `caller` | The caller's ARN |
976
+ | `user` | The caller's ARN |
977
+ | `userArn` | The caller's ARN |
978
+
979
+ Real API Gateway puts the unique id of the principal in `caller` and `user`, such as `AIDA...` for a
980
+ User. A request carries no such id into the simulation, so the ARN identifying the caller goes in
981
+ every field that can be filled from it.
982
+
983
+ A method of any other authorization type leaves those four `null`, and so does an `AWS_IAM` method
984
+ called by a principal with no ARN behind it. `accessKey`, `apiKey`, `apiKeyId`, `principalOrgId` and
985
+ the two Cognito identity pool fields are `null` throughout. `sourceIp` and `userAgent` describe the
986
+ request itself and are filled for every method.
987
+
988
+ ## Authorizing a method with a user pool
989
+
990
+ A `COGNITO_USER_POOLS` authorizer verifies the token itself against the keys the user pools it names
991
+ publish. Nothing is invoked, so there is no function to write and no policy to answer.
992
+ `CreateAuthorizerCommand` takes the pools as `providerARNs`, and `PutMethodCommand` binds the
993
+ authorizer to a method with `authorizationType: "COGNITO_USER_POOLS"`.
994
+
995
+ ```typescript sim-apigateway-cognito-authorizer
996
+ /**
997
+ * Gating a REST API method with a Cognito user pool authorizer.
998
+ *
999
+ * The authorizer verifies the token against the keys the pool publishes, and
1000
+ * the token's own claims reach the handler under `requestContext.authorizer`.
1001
+ */
1002
+
1003
+ import {
1004
+ AdminCreateUserCommand,
1005
+ AdminInitiateAuthCommand,
1006
+ AdminSetUserPasswordCommand,
1007
+ CreateUserPoolClientCommand,
1008
+ CreateUserPoolCommand,
1009
+ } from "@aws-sdk/client-cognito-identity-provider";
1010
+
1011
+ import { SimAws } from "@kensio/yulin";
1012
+ import { simRestApiLambdaProxyFactory } from "@kensio/yulin/apigateway";
1013
+ import { serveSimAws } from "@kensio/yulin/serve";
1014
+
1015
+ const simAws = new SimAws();
1016
+ const cognito = simAws.cognitoIdentityProvider();
1017
+
1018
+ const pool = await cognito.createUserPool(
1019
+ new CreateUserPoolCommand({ PoolName: "myapp-users" }),
1020
+ );
1021
+ const UserPoolId = pool.UserPool!.Id!;
1022
+
1023
+ const appClient = await cognito.createUserPoolClient(
1024
+ new CreateUserPoolClientCommand({
1025
+ UserPoolId,
1026
+ ClientName: "web",
1027
+ ExplicitAuthFlows: ["ALLOW_ADMIN_USER_PASSWORD_AUTH"],
1028
+ }),
1029
+ );
1030
+ const ClientId = appClient.UserPoolClient!.ClientId!;
1031
+
1032
+ await cognito.adminCreateUser(
1033
+ new AdminCreateUserCommand({ UserPoolId, Username: "ada" }),
1034
+ );
1035
+ await cognito.adminSetUserPassword(
1036
+ new AdminSetUserPasswordCommand({
1037
+ UserPoolId,
1038
+ Username: "ada",
1039
+ Password: "Correct-horse-1",
1040
+ Permanent: true,
1041
+ }),
1042
+ );
1043
+
1044
+ const restApi = await simRestApiLambdaProxyFactory.make(
1045
+ {
1046
+ resourcePaths: ["/orders"],
1047
+ cognitoUserPoolArns: [pool.UserPool!.Arn!],
1048
+ handler: (event) => ({
1049
+ statusCode: 200,
1050
+ headers: { "content-type": "text/plain" },
1051
+ body: `orders for ${
1052
+ event.requestContext.authorizer?.claims?.["cognito:username"] ??
1053
+ "nobody"
1054
+ }`,
1055
+ }),
1056
+ },
1057
+ simAws,
1058
+ );
1059
+
1060
+ const signedIn = await cognito.adminInitiateAuth(
1061
+ new AdminInitiateAuthCommand({
1062
+ UserPoolId,
1063
+ ClientId,
1064
+ AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
1065
+ AuthParameters: { USERNAME: "ada", PASSWORD: "Correct-horse-1" },
1066
+ }),
1067
+ );
1068
+ const idToken = signedIn.AuthenticationResult!.IdToken!;
1069
+
1070
+ const srv = await serveSimAws({ simAws });
1071
+ const url = srv.localUrl(`${restApi.invokeUrl("prod")}/orders`);
1072
+
1073
+ const anonymous = await fetch(url);
1074
+
1075
+ console.log(anonymous.status);
1076
+ // 401
1077
+
1078
+ const authorized = await fetch(url, { headers: { authorization: idToken } });
1079
+
1080
+ console.log(await authorized.text());
1081
+ // "orders for ada"
1082
+
1083
+ // Advancing the simulation's clock past the token's expiry closes the method
1084
+ // to the same token, with nothing reissued.
1085
+ await simAws.clock().advanceBy({ hours: 2 });
1086
+
1087
+ const expired = await fetch(url, { headers: { authorization: idToken } });
1088
+
1089
+ console.log(expired.status);
1090
+ // 401
1091
+
1092
+ await srv.close();
1093
+ ```
1094
+
1095
+ `simRestApiLambdaProxyFactory` builds the authorizer and the methods bound to it when it is given
1096
+ `cognitoUserPoolArns`.
1097
+
1098
+ A `providerARN` is read for the pool id it names, and the pool is looked up across every simulated
1099
+ account. The token is accepted when any one of the named pools signed it, its `iss` names that same
1100
+ pool, and its time claims hold against the simulation's clock. Advancing the clock past a token's
1101
+ `exp` therefore closes a method that was open to it.
1102
+
1103
+ ### The claims the handler receives
1104
+
1105
+ The token's own claims reach the handler under `requestContext.authorizer.claims`, and every value
1106
+ arrives as a string. A list claim such as `cognito:groups` is rendered the way Go prints a slice, so
1107
+ two groups arrive as `[Admins Readers]`.
1108
+
1109
+ ```json
1110
+ {
1111
+ "claims": {
1112
+ "sub": "5c4a5f6c-6c31-4a2e-9a55-2c4dcb8f2f4f",
1113
+ "cognito:username": "ada",
1114
+ "cognito:groups": "[Admins Readers]",
1115
+ "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_aBcDeFgHi",
1116
+ "token_use": "id"
1117
+ }
1118
+ }
1119
+ ```
1120
+
1121
+ An HTTP API puts the same claims under `requestContext.authorizer.jwt.claims`, with the scopes
1122
+ beside them, so a handler moved between the two reads a different shape.
1123
+
1124
+ ### Scopes
1125
+
1126
+ `authorizationScopes` on the method is met by any one of the scopes the token's `scope` claim
1127
+ carries. The scopes are the method's own, so one authorizer covers methods asking for different
1128
+ ones.
1129
+
1130
+ A method asking for no scope takes an id token and an access token alike, because `token_use` is not
1131
+ checked, which is what real API Gateway does. A method asking for a scope takes only an access
1132
+ token, since an id token carries no `scope` claim at all.
1133
+
1134
+ ### What a refused request gets back
1135
+
1136
+ | Case | Answer |
1137
+ | ---------------------------------------------------------- | ---------------------------------------------------- |
1138
+ | No value at the identity source | 401 `Unauthorized` |
1139
+ | A value that is not a readable JWT | 401 `Unauthorized` |
1140
+ | A token no named pool signed, or one signed by another key | 401 `Unauthorized` |
1141
+ | A token that has expired, or has no `exp` at all | 401 `Unauthorized` |
1142
+ | A verified token claiming none of the method's scopes | 403 `User is not authorized to access this resource` |
1143
+
1144
+ Every refusal up to and including the claim checks is the same 401, so a client learns that its
1145
+ token was not accepted and nothing about which check it failed. An unmet scope is the one 403: the
1146
+ token was accepted, and it does not allow this method.
1147
+
1148
+ The token is taken with or without the `Bearer` scheme in front of it. The identity source is one
1149
+ header, as it is for a `TOKEN` authorizer.
1150
+
1151
+ ## Protecting a stage with a web ACL
1152
+
1153
+ A simulated WAFv2 web ACL can go in front of a stage, and every request that stage serves is then
1154
+ put through its rules. `AssociateWebACL` names the stage by its ARN, which `SimRestApi.stageArn`
1155
+ builds.
1156
+
1157
+ The web ACL sees the request before the method is matched and before any authorizer runs, ahead of
1158
+ IAM, a Lambda authorizer and a Cognito authorizer alike. A blocked request gets 403 with WAF's body,
1159
+ and the integration is never invoked. See
1160
+ [Protecting an API Gateway REST API stage](https://yulinsim.dev/services/wafv2/#protecting-an-api-gateway-rest-api-stage) in
1161
+ the WAFv2 docs.
1162
+
1163
+ ## Intercepting an SDK client
1164
+
1165
+ `SimSdk` routes `@aws-sdk/client-api-gateway` commands to the simulation. Code under test builds its
1166
+ own client and reaches simulated API Gateway through it.
1167
+
1168
+ ```typescript sim-apigateway-sdk-interception
1169
+ /**
1170
+ * Reaching simulated API Gateway through a real APIGatewayClient.
1171
+ */
1172
+
1173
+ import {
1174
+ APIGatewayClient,
1175
+ CreateRestApiCommand,
1176
+ GetRestApisCommand,
1177
+ } from "@aws-sdk/client-api-gateway";
1178
+
1179
+ import { SimAws } from "@kensio/yulin";
1180
+ import { SimSdk } from "@kensio/yulin/sdk";
1181
+
1182
+ const simAws = new SimAws();
1183
+ using simSdk = new SimSdk({ simAws });
1184
+
1185
+ const client = new APIGatewayClient({ region: "eu-west-2" });
1186
+ simSdk.intercept(client);
1187
+
1188
+ await client.send(new CreateRestApiCommand({ name: "orders" }));
1189
+
1190
+ const listed = await client.send(new GetRestApisCommand({}));
1191
+ console.log(listed.items?.map((restApi) => restApi.name));
1192
+ // [ "orders" ]
1193
+ ```
1194
+
1195
+ The client's region decides which simulated account and region scope the API lands in, the same way
1196
+ it does for every other intercepted service.
1197
+
1198
+ ## Importing an OpenAPI definition
1199
+
1200
+ `ImportRestApiCommand` takes a serialised OpenAPI 3.0 document and creates the API, the resources of
1201
+ its path tree, one method per operation and the integration behind each method. Every segment of a
1202
+ path becomes a resource, and paths sharing a prefix share the nodes that spell it, so
1203
+ `/pets/{petId}` is a `{petId}` resource under a `pets` one under the root.
1204
+
1205
+ An import creates no stage. `CreateDeploymentCommand` or an `AWS::ApiGateway::Stage` is still
1206
+ declared separately, and an imported API with no stage answers 403.
1207
+
1208
+ ```typescript sim-apigateway-openapi
1209
+ /**
1210
+ * Creating a simulated REST API from an OpenAPI 3 definition.
1211
+ */
1212
+
1213
+ import {
1214
+ CreateDeploymentCommand,
1215
+ GetResourcesCommand,
1216
+ ImportRestApiCommand,
1217
+ } from "@aws-sdk/client-api-gateway";
1218
+ import {
1219
+ AddPermissionCommand,
1220
+ CreateFunctionCommand,
1221
+ } from "@aws-sdk/client-lambda";
1222
+
1223
+ import { SimAws } from "@kensio/yulin";
1224
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1225
+ import { serveSimAws, type SimPayload1Event } from "@kensio/yulin/serve";
1226
+
1227
+ const simAws = new SimAws();
1228
+
1229
+ const { FunctionArn } = await simAws.lambda().createFunction(
1230
+ new CreateFunctionCommand({
1231
+ FunctionName: "pets",
1232
+ Role: "arn:aws:iam::111111111111:role/PetsRole",
1233
+ Code: {
1234
+ ZipFile: makeLambdaZipFileInput((event: SimPayload1Event) => ({
1235
+ statusCode: 200,
1236
+ headers: { "content-type": "text/plain" },
1237
+ body: `pet ${event.pathParameters?.["petId"] ?? "none"}`,
1238
+ })),
1239
+ },
1240
+ }),
1241
+ );
1242
+
1243
+ const openApi = {
1244
+ openapi: "3.0.1",
1245
+ info: { title: "pets", version: "1.0" },
1246
+ paths: {
1247
+ "/pets/{petId}": {
1248
+ get: {
1249
+ // Ignored, as on AWS, since no request validator names this schema.
1250
+ responses: { "200": { description: "200 response" } },
1251
+ "x-amazon-apigateway-integration": {
1252
+ type: "aws_proxy",
1253
+ httpMethod: "POST",
1254
+ uri:
1255
+ `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/` +
1256
+ `${FunctionArn}/invocations`,
1257
+ },
1258
+ },
1259
+ },
1260
+ },
1261
+ };
1262
+
1263
+ const apiGateway = simAws.apiGateway();
1264
+ const definition = new TextEncoder().encode(JSON.stringify(openApi));
1265
+
1266
+ const { id: restApiId } = await apiGateway.importRestApi(
1267
+ new ImportRestApiCommand({ body: definition }),
1268
+ );
1269
+
1270
+ const resources = await apiGateway.getResources(
1271
+ new GetResourcesCommand({ restApiId }),
1272
+ );
1273
+
1274
+ console.log(resources.items.map((resource) => resource.path));
1275
+ // [ "/", "/pets", "/pets/{petId}" ]
1276
+
1277
+ // An import creates no stage. The API answers 403 until one is deployed.
1278
+ await apiGateway.createDeployment(
1279
+ new CreateDeploymentCommand({ restApiId, stageName: "prod" }),
1280
+ );
1281
+
1282
+ await simAws.lambda().addPermission(
1283
+ new AddPermissionCommand({
1284
+ FunctionName: "pets",
1285
+ StatementId: "api-gateway-invoke",
1286
+ Action: "lambda:InvokeFunction",
1287
+ Principal: "apigateway.amazonaws.com",
1288
+ SourceArn: `arn:aws:execute-api:us-east-1:888888888888:${restApiId}/*/*/*`,
1289
+ }),
1290
+ );
1291
+
1292
+ const srv = await serveSimAws({ simAws });
1293
+
1294
+ const response = await fetch(
1295
+ srv.localUrl(
1296
+ `https://${restApiId}.execute-api.us-east-1.amazonaws.com/prod/pets/42`,
1297
+ ),
1298
+ );
1299
+
1300
+ console.log(await response.text());
1301
+ // "pet 42"
1302
+
1303
+ await srv.close();
1304
+ ```
1305
+
1306
+ The API is named by `info.title`. `uri` is read as either the long
1307
+ `arn:aws:apigateway:<region>:lambda:path/2015-03-31/functions/<function-arn>/invocations` form above
1308
+ or as the bare function ARN. An operation carrying no `security` becomes an open method, and one
1309
+ naming a security scheme is gated by the authorizer that scheme declares. See
1310
+ [Security schemes](#security-schemes).
1311
+
1312
+ ### The catch-all operation key
1313
+
1314
+ `x-amazon-apigateway-any-method` declares an `ANY` method on the path. That method serves every verb
1315
+ the resource has no method of its own for, and OpenAPI has no operation key of its own for it:
1316
+
1317
+ ```json
1318
+ {
1319
+ "/pets": {
1320
+ "x-amazon-apigateway-any-method": {
1321
+ "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "..." }
1322
+ }
1323
+ }
1324
+ }
1325
+ ```
1326
+
1327
+ A `{proxy+}` segment becomes a greedy resource that matches the rest of the request path. One path
1328
+ of `/{proxy+}` carrying that extension is the whole of what CDK's `LambdaRestApi` builds.
1329
+
1330
+ ### Security schemes
1331
+
1332
+ A `components.securitySchemes` member carrying `x-amazon-apigateway-authtype` becomes an authorizer,
1333
+ and `security` on an operation puts that authorizer in front of the method. The scheme key names it.
1334
+ One authorizer is created per scheme, shared by every operation naming it.
1335
+
1336
+ ```typescript sim-apigateway-openapi-authorizer
1337
+ /**
1338
+ * Importing a REST API whose method is gated by a security scheme.
1339
+ */
1340
+
1341
+ import {
1342
+ GetAuthorizersCommand,
1343
+ GetMethodCommand,
1344
+ GetResourcesCommand,
1345
+ ImportRestApiCommand,
1346
+ } from "@aws-sdk/client-api-gateway";
1347
+ import { CreateFunctionCommand } from "@aws-sdk/client-lambda";
1348
+
1349
+ import { SimAws } from "@kensio/yulin";
1350
+ import type { SimRestApiTokenAuthorizerEvent } from "@kensio/yulin/apigateway";
1351
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1352
+
1353
+ const simAws = new SimAws();
1354
+
1355
+ const { FunctionArn: petsArn } = await simAws.lambda().createFunction(
1356
+ new CreateFunctionCommand({
1357
+ FunctionName: "pets",
1358
+ Role: "arn:aws:iam::111111111111:role/PetsRole",
1359
+ Code: {
1360
+ ZipFile: makeLambdaZipFileInput(() => ({
1361
+ statusCode: 200,
1362
+ body: "pets",
1363
+ })),
1364
+ },
1365
+ }),
1366
+ );
1367
+
1368
+ const { FunctionArn: authorizerArn } = await simAws.lambda().createFunction(
1369
+ new CreateFunctionCommand({
1370
+ FunctionName: "pet-authorizer",
1371
+ Role: "arn:aws:iam::111111111111:role/PetsRole",
1372
+ Code: {
1373
+ ZipFile: makeLambdaZipFileInput(
1374
+ (event: SimRestApiTokenAuthorizerEvent) => ({
1375
+ principalId: "pet-owner",
1376
+ policyDocument: {
1377
+ Version: "2012-10-17",
1378
+ Statement: [
1379
+ {
1380
+ Action: "execute-api:Invoke",
1381
+ Effect:
1382
+ event.authorizationToken === "Bearer valid"
1383
+ ? "Allow"
1384
+ : "Deny",
1385
+ Resource: event.methodArn,
1386
+ },
1387
+ ],
1388
+ },
1389
+ }),
1390
+ ),
1391
+ },
1392
+ }),
1393
+ );
1394
+
1395
+ const openApi = {
1396
+ openapi: "3.0.1",
1397
+ info: { title: "pets", version: "1.0" },
1398
+ paths: {
1399
+ "/pets": {
1400
+ get: {
1401
+ security: [{ "pet-authorizer": [] }],
1402
+ "x-amazon-apigateway-integration": {
1403
+ type: "aws_proxy",
1404
+ httpMethod: "POST",
1405
+ uri: petsArn,
1406
+ },
1407
+ },
1408
+ },
1409
+ },
1410
+ components: {
1411
+ securitySchemes: {
1412
+ "pet-authorizer": {
1413
+ type: "apiKey",
1414
+ name: "Authorization",
1415
+ in: "header",
1416
+ "x-amazon-apigateway-authtype": "custom",
1417
+ "x-amazon-apigateway-authorizer": {
1418
+ type: "token",
1419
+ authorizerUri: authorizerArn,
1420
+ authorizerResultTtlInSeconds: 300,
1421
+ },
1422
+ },
1423
+ },
1424
+ },
1425
+ };
1426
+
1427
+ const apiGateway = simAws.apiGateway();
1428
+
1429
+ const definition = new TextEncoder().encode(JSON.stringify(openApi));
1430
+
1431
+ const { id: restApiId } = await apiGateway.importRestApi(
1432
+ new ImportRestApiCommand({ body: definition }),
1433
+ );
1434
+
1435
+ const authorizers = await apiGateway.getAuthorizers(
1436
+ new GetAuthorizersCommand({ restApiId }),
1437
+ );
1438
+
1439
+ console.log(authorizers.items.map((one) => [one.name, one.type]));
1440
+ // [ [ "pet-authorizer", "TOKEN" ] ]
1441
+
1442
+ const resources = await apiGateway.getResources(
1443
+ new GetResourcesCommand({ restApiId }),
1444
+ );
1445
+ const pets = resources.items.find((resource) => resource.path === "/pets");
1446
+
1447
+ const method = await apiGateway.getMethod(
1448
+ new GetMethodCommand({ restApiId, resourceId: pets?.id, httpMethod: "GET" }),
1449
+ );
1450
+
1451
+ console.log(method.authorizationType);
1452
+ // "CUSTOM"
1453
+ ```
1454
+
1455
+ Three `x-amazon-apigateway-authtype` values are read, and each writes a different kind of gate on
1456
+ the method.
1457
+
1458
+ | `x-amazon-apigateway-authtype` | The authorizer `type` under it | Method `AuthorizationType` |
1459
+ | ------------------------------ | ------------------------------ | -------------------------- |
1460
+ | `custom` | `token` or `request` | `CUSTOM` |
1461
+ | `cognito_user_pools` | `cognito_user_pools` | `COGNITO_USER_POOLS` |
1462
+ | `awsSigv4` | (the scheme carries none) | `AWS_IAM` |
1463
+
1464
+ A `cognito_user_pools` authorizer carries the `providerARNs` of the pools it accepts tokens from,
1465
+ and the scopes a requirement asks for (`security: [{ "pet-authorizer": ["pets.read"] }]`) become the
1466
+ method's `authorizationScopes`. `PutMethod` refuses scopes on every other kind of method, since a
1467
+ token is what they are checked against.
1468
+
1469
+ An `awsSigv4` scheme names a header and leaves the deciding to IAM. `x-amazon-apigateway-auth` on an
1470
+ operation asks for the same gate, and is what an API Gateway console export writes:
1471
+
1472
+ ```json
1473
+ { "x-amazon-apigateway-auth": { "type": "AWS_IAM" } }
1474
+ ```
1475
+
1476
+ A `token` or `cognito_user_pools` authorizer reads the one header the scheme's own `name` and `in`
1477
+ name, which for the scheme above is `method.request.header.Authorization`. That is where AWS reads
1478
+ it from, and an `identitySource` written inside either authorizer is refused. A `request` authorizer
1479
+ names its own `identitySource`, with as many comma-separated expressions as identify its callers.
1480
+
1481
+ `authorizerResultTtlInSeconds` on a Lambda authorizer is how long its decisions are held for, the
1482
+ same as it is on `CreateAuthorizer`. See
1483
+ [Caching the authorizer's decision](#caching-the-authorizers-decision).
1484
+
1485
+ A scheme whose `x-amazon-apigateway-authtype` and whose authorizer `type` disagree is refused,
1486
+ naming the pointer of the member they disagree about, such as
1487
+ `#/components/securitySchemes/pet-authorizer/x-amazon-apigateway-authorizer/type`. These are refused
1488
+ where a document writes them too:
1489
+
1490
+ - An `apiKey` scheme carrying no `x-amazon-apigateway-authtype`. That is an API key, and API keys
1491
+ and usage plans are outside this simulation.
1492
+ - An `http`, `oauth2` or `openIdConnect` scheme. A REST API method is gated by the three authtypes
1493
+ above and by nothing else.
1494
+ - `authorizerCredentials` and `identityValidationExpression` on an authorizer. `CreateAuthorizer`
1495
+ refuses both of them as well.
1496
+ - A `security` requirement at the root of the document. Write the requirement on each operation.
1497
+ - An operation naming two requirements, or one requirement naming two schemes. One authorizer
1498
+ decides a method.
1499
+
1500
+ ### Members that are ignored
1501
+
1502
+ AWS sorts what an import finds into three categories, and the third is valid OpenAPI a REST API
1503
+ leaves unsupported without a request validator. AWS ignores it silently, and so does this:
1504
+ `requestBody`, the content schemas under `responses`, `components.schemas`, and an operation's
1505
+ `parameters`, `summary`, `description` and `tags`. Request validation is refused at the root of the
1506
+ document, so a request whose body contradicts a declared schema still reaches the handler.
1507
+ `operationId` is ignored too, since it only supplies the `OperationName` a method carries for
1508
+ documentation.
1509
+
1510
+ Everything else the document carries and this simulation cannot apply is refused, naming the JSON
1511
+ pointer of the member, such as
1512
+ `#/paths/~1pets~1{petId}/get/x-amazon-apigateway-integration/passthroughBehavior`.
1513
+
1514
+ ### Replacing a definition
1515
+
1516
+ `PutRestApiCommand` with `mode: "overwrite"` replaces an API's whole definition with the document's.
1517
+ The API keeps its id, its endpoint and the stages serving it. Its path tree and its authorizers are
1518
+ built again from the document, and an authorizer the previous definition left behind goes with them.
1519
+
1520
+ ```typescript
1521
+ await apiGateway.putRestApi(
1522
+ new PutRestApiCommand({ restApiId, mode: "overwrite", body: definition }),
1523
+ );
1524
+ ```
1525
+
1526
+ `mode: "merge"` is refused, and so is a `PutRestApi` that leaves the mode out, which AWS reads as a
1527
+ merge. A merge adds the document's paths to the API's existing ones, and which of two declarations
1528
+ of one method it keeps decides what every request to that method reaches.
1529
+
1530
+ A refused replacement leaves the API with an empty path tree. The old definition has already been
1531
+ taken out by the time a member deep in the document is refused, and an API serving nothing is
1532
+ clearer than one serving half of each document.
1533
+
1534
+ ## Deploying from CloudFormation and CDK
1535
+
1536
+ [Simulated CloudFormation](https://yulinsim.dev/services/cloudformation/ "Simulated CloudFormation docs") deploys
1537
+ `AWS::ApiGateway::RestApi`, `AWS::ApiGateway::Resource`, `AWS::ApiGateway::Method`,
1538
+ `AWS::ApiGateway::Deployment` and `AWS::ApiGateway::Stage`. A synthesized or hand-written template
1539
+ produces an API that serves requests.
1540
+
1541
+ `Ref` and `Fn::GetAtt` return what real CloudFormation returns for each type:
1542
+
1543
+ | Resource type | `Ref` | `Fn::GetAtt` |
1544
+ | ------------- | ----------------- | ----------------------------- |
1545
+ | `RestApi` | the API id | `RestApiId`, `RootResourceId` |
1546
+ | `Resource` | the resource id | `ResourceId` |
1547
+ | `Method` | the logical id | none, as AWS documents none |
1548
+ | `Deployment` | the deployment id | `DeploymentId` |
1549
+ | `Stage` | the stage name | none, as AWS documents none |
1550
+
1551
+ A REST API method has no id of its own. It is addressed by its API, its resource and its HTTP verb,
1552
+ and a `Ref` to one falls back on the CloudFormation logical id. CDK reads that value only to publish
1553
+ `Method.methodId`.
1554
+
1555
+ The API publishes no endpoint attribute either, because real API Gateway reports none. CDK joins the
1556
+ URL out of a `Ref` to the API, the region, `AWS::URLSuffix` and a `Ref` to the stage. That suffix
1557
+ resolves to the local `sim-aws.localhost` hostname, and the stage is the first path segment of what
1558
+ it builds.
1559
+
1560
+ A `Resource` names its place in the tree through `ParentId`. The top of the tree reads
1561
+ `Fn::GetAtt: ["<Api>", "RootResourceId"]`, and everything below it a `Ref` to the node above.
1562
+
1563
+ A `Method` carries its integration as an `Integration` block of its own, which is how the REST API
1564
+ models one. The block becomes the `PutIntegration` that follows the method's `PutMethod`, and its
1565
+ `Uri` is read as the bare Lambda function ARN or as the
1566
+ `arn:aws:apigateway:<region>:lambda:path/2015-03-31/functions/<function-arn>/invocations` string CDK
1567
+ builds with `Fn::Join`.
1568
+
1569
+ ```typescript sim-apigateway-cloudformation
1570
+ /**
1571
+ * Deploying a simulated REST API from a CloudFormation template.
1572
+ */
1573
+
1574
+ import { SimAws } from "@kensio/yulin";
1575
+ import { serveSimAws } from "@kensio/yulin/serve";
1576
+
1577
+ const simAws = new SimAws();
1578
+
1579
+ const stack = await simAws.cloudFormation().deployTemplate({
1580
+ stackName: "orders-stack",
1581
+ template: {
1582
+ Resources: {
1583
+ HandlerRole: {
1584
+ Type: "AWS::IAM::Role",
1585
+ Properties: {
1586
+ RoleName: "orders-role",
1587
+ AssumeRolePolicyDocument: {
1588
+ Version: "2012-10-17",
1589
+ Statement: [
1590
+ {
1591
+ Effect: "Allow",
1592
+ Principal: { Service: "lambda.amazonaws.com" },
1593
+ Action: "sts:AssumeRole",
1594
+ },
1595
+ ],
1596
+ },
1597
+ },
1598
+ },
1599
+ Handler: {
1600
+ Type: "AWS::Lambda::Function",
1601
+ Properties: {
1602
+ FunctionName: "orders",
1603
+ Role: { "Fn::GetAtt": ["HandlerRole", "Arn"] },
1604
+ Handler: "index.handler",
1605
+ Runtime: "nodejs20.x",
1606
+ Code: {
1607
+ ZipFile:
1608
+ "exports.handler = async (event) => ({ statusCode: 200, body: 'order ' + event.pathParameters.orderId });",
1609
+ },
1610
+ },
1611
+ },
1612
+ HandlerPermission: {
1613
+ Type: "AWS::Lambda::Permission",
1614
+ Properties: {
1615
+ Action: "lambda:InvokeFunction",
1616
+ FunctionName: { "Fn::GetAtt": ["Handler", "Arn"] },
1617
+ Principal: "apigateway.amazonaws.com",
1618
+ SourceArn: {
1619
+ "Fn::Join": [
1620
+ "",
1621
+ [
1622
+ "arn:aws:execute-api:",
1623
+ { Ref: "AWS::Region" },
1624
+ ":",
1625
+ { Ref: "AWS::AccountId" },
1626
+ ":",
1627
+ { Ref: "Api" },
1628
+ "/*/*/*",
1629
+ ],
1630
+ ],
1631
+ },
1632
+ },
1633
+ },
1634
+ Api: {
1635
+ Type: "AWS::ApiGateway::RestApi",
1636
+ Properties: { Name: "orders" },
1637
+ },
1638
+ OrdersResource: {
1639
+ Type: "AWS::ApiGateway::Resource",
1640
+ Properties: {
1641
+ RestApiId: { Ref: "Api" },
1642
+ ParentId: { "Fn::GetAtt": ["Api", "RootResourceId"] },
1643
+ PathPart: "orders",
1644
+ },
1645
+ },
1646
+ OrderResource: {
1647
+ Type: "AWS::ApiGateway::Resource",
1648
+ Properties: {
1649
+ RestApiId: { Ref: "Api" },
1650
+ ParentId: { Ref: "OrdersResource" },
1651
+ PathPart: "{orderId}",
1652
+ },
1653
+ },
1654
+ GetOrder: {
1655
+ Type: "AWS::ApiGateway::Method",
1656
+ Properties: {
1657
+ RestApiId: { Ref: "Api" },
1658
+ ResourceId: { Ref: "OrderResource" },
1659
+ HttpMethod: "GET",
1660
+ AuthorizationType: "NONE",
1661
+ Integration: {
1662
+ Type: "AWS_PROXY",
1663
+ IntegrationHttpMethod: "POST",
1664
+ Uri: {
1665
+ "Fn::Join": [
1666
+ "",
1667
+ [
1668
+ "arn:aws:apigateway:",
1669
+ { Ref: "AWS::Region" },
1670
+ ":lambda:path/2015-03-31/functions/",
1671
+ { "Fn::GetAtt": ["Handler", "Arn"] },
1672
+ "/invocations",
1673
+ ],
1674
+ ],
1675
+ },
1676
+ },
1677
+ },
1678
+ },
1679
+ Deployment: {
1680
+ Type: "AWS::ApiGateway::Deployment",
1681
+ Properties: { RestApiId: { Ref: "Api" } },
1682
+ DependsOn: ["GetOrder"],
1683
+ },
1684
+ Stage: {
1685
+ Type: "AWS::ApiGateway::Stage",
1686
+ Properties: {
1687
+ RestApiId: { Ref: "Api" },
1688
+ DeploymentId: { Ref: "Deployment" },
1689
+ StageName: "prod",
1690
+ },
1691
+ },
1692
+ },
1693
+ Outputs: {
1694
+ ApiUrl: {
1695
+ Value: {
1696
+ "Fn::Join": [
1697
+ "",
1698
+ [
1699
+ "https://",
1700
+ { Ref: "Api" },
1701
+ ".execute-api.",
1702
+ { Ref: "AWS::Region" },
1703
+ ".",
1704
+ { Ref: "AWS::URLSuffix" },
1705
+ "/",
1706
+ { Ref: "Stage" },
1707
+ "/",
1708
+ ],
1709
+ ],
1710
+ },
1711
+ },
1712
+ },
1713
+ },
1714
+ });
1715
+
1716
+ await stack.waitForDeployComplete();
1717
+
1718
+ // https://<api-id>.execute-api.us-east-1.sim-aws.localhost/prod/
1719
+ const apiUrl = stack.output("ApiUrl");
1720
+
1721
+ const srv = await serveSimAws({ simAws });
1722
+
1723
+ const response = await fetch(srv.localUrl(`${apiUrl}orders/6`));
1724
+
1725
+ console.log(response.status);
1726
+ // 200
1727
+
1728
+ console.log(await response.text());
1729
+ // "order 6"
1730
+
1731
+ await srv.close();
1732
+ ```
1733
+
1734
+ Every property outside the simulated set is left out of what is created and recorded in
1735
+ [`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without),
1736
+ naming the Resource type, the logical id and the ones this can act on. The API, resource, method,
1737
+ deployment or stage is created either way. The stack deploys, and the record says which of its parts
1738
+ behaves differently to the template. The simulated properties are:
1739
+
1740
+ - `RestApi`: `Name`, `Description`, `DisableExecuteApiEndpoint`, `Body`, `FailOnWarnings`
1741
+ - `Resource`: `RestApiId`, `ParentId`, `PathPart`
1742
+ - `Method`: `RestApiId`, `ResourceId`, `HttpMethod`, `AuthorizationType`, `AuthorizerId`,
1743
+ `AuthorizationScopes`, `ApiKeyRequired`, `OperationName`, `Integration`
1744
+ - A method's `Integration` block: `Type`, `IntegrationHttpMethod`, `Uri`
1745
+ - `Authorizer`: `RestApiId`, `Name`, `Type`, `AuthorizerUri`, `ProviderARNs`, `IdentitySource`
1746
+ - `Deployment`: `RestApiId`, `Description`, `StageName`
1747
+ - `Stage`: `RestApiId`, `DeploymentId`, `StageName`, `Description`, `Variables`,
1748
+ `MethodSettings`
1749
+ - A stage's `MethodSettings` entries: `ResourcePath`, `HttpMethod`, `ThrottlingRateLimit`,
1750
+ `ThrottlingBurstLimit`
1751
+
1752
+ A `Body` on the `RestApi` is an inline OpenAPI document declaring the API's resources, methods and
1753
+ integrations. It goes through `ImportRestApi`, the same translator an SDK caller importing a
1754
+ document reaches, and the template then carries no `Resource` or `Method` of its own. See
1755
+ [Importing an OpenAPI definition](#importing-an-openapi-definition) for what the document may hold.
1756
+
1757
+ ```yaml
1758
+ Api:
1759
+ Type: AWS::ApiGateway::RestApi
1760
+ Properties:
1761
+ Body:
1762
+ openapi: 3.0.1
1763
+ info: { title: pets, version: "1.0" }
1764
+ paths:
1765
+ /pets/{petId}:
1766
+ get:
1767
+ x-amazon-apigateway-integration:
1768
+ type: aws_proxy
1769
+ httpMethod: POST
1770
+ uri: !GetAtt Handler.Arn
1771
+ ```
1772
+
1773
+ `Name` names the API where the template carries both, and the document's `info.title` names it
1774
+ otherwise. `Description` and `DisableExecuteApiEndpoint` beside a `Body` are recorded and left off
1775
+ the API, because `ImportRestApi` takes neither and AWS applies them in a second step. A
1776
+ `Resource`, `Method` or other entry adding to an API a `Body` already declared fails the stack
1777
+ naming both, since the template would then declare the API two ways at once. `BodyS3Location` and
1778
+ `Mode` are recorded like any other unsimulated property.
1779
+
1780
+ A `Body` holding a Swagger 2.0 document is the one document that is recorded instead of refused. The
1781
+ API deploys with an empty path tree and the record says why. SAM writes Swagger 2.0 for an
1782
+ `AWS::Serverless::Api` unless the template asks for `OpenApiVersion: 3.0.1`, and failing the stack
1783
+ over the version of a document would take a whole SAM API down with it. Every other document the
1784
+ import refuses fails the Resource, because an API deployed with an empty tree answers 403 for every
1785
+ path the document declared.
1786
+
1787
+ `StageName` on a `Deployment` is the older one-Resource form, where the deployment publishes a stage
1788
+ of that name by itself and the template carries one Resource fewer.
1789
+
1790
+ ### SAM
1791
+
1792
+ A template naming the SAM transform reaches the same resource types.
1793
+ `AWS::Serverless::Api` becomes a REST API with its deployment and stage, and the `Api` event of an
1794
+ `AWS::Serverless::Function` becomes the path resources, the method and the invoke permission that
1795
+ put the function behind it. Events naming no `RestApiId` share one API on a `Prod` stage.
1796
+ [The SAM section of the CloudFormation docs](https://yulinsim.dev/services/cloudformation/#rest-apis) covers both.
1797
+
1798
+ ### CDK
1799
+
1800
+ CDK's `RestApi` and `LambdaRestApi` both deploy and serve. `LambdaRestApi` synthesizes an `ANY`
1801
+ method on the root and another on a `{proxy+}` resource, both in front of one function, with the
1802
+ `AWS::Lambda::Permission` each needs. `restApi.url` and `restApi.urlForPath` resolve to the local
1803
+ hostname through `AWS::URLSuffix`.
1804
+
1805
+ `TokenAuthorizer` and `RequestAuthorizer` deploy too, with the `AWS::Lambda::Permission` each
1806
+ writes for its own function. CDK holds a decision for five minutes by default, and
1807
+ `resultsCacheTtl` sets that period. See
1808
+ [Caching the authorizer's decision](#caching-the-authorizers-decision).
1809
+
1810
+ `CognitoUserPoolsAuthorizer` deploys as it stands, naming the pools it was given by ARN. A method
1811
+ takes it with `authorizationType: apigateway.AuthorizationType.COGNITO`, and a user pool the same
1812
+ stack declares is reached through the `Fn::GetAtt` on its ARN that CDK writes.
1813
+
1814
+ CDK also writes an `AWS::ApiGateway::Account` and a CloudWatch role beside a default `RestApi`.
1815
+ Neither is simulated. The `Account` Resource is recorded in
1816
+ [`stack.skippedResources`](https://yulinsim.dev/services/cloudformation/#inspecting-stacks-and-resources) and the rest of the
1817
+ stack deploys.
1818
+
1819
+ `ApiKey`, `UsagePlan`, `UsagePlanKey`, `RequestValidator`, `Model`, `DomainName` and
1820
+ `BasePathMapping` are recorded there too, each naming the reason. A method under one of them is
1821
+ refused by `PutMethod`, because a method that looked gated to the template and answered every
1822
+ request here is worse than a failed deployment.
1823
+
1824
+ ## Authorization
1825
+
1826
+ Every command is authorized by simulated IAM, and API Gateway asks IAM an unusual question. The
1827
+ action is the HTTP method of the underlying REST call, `apigateway:POST` and friends, and the
1828
+ resource is the request path. `CreateResource` on API `a1b2c3d4e5` asks whether the caller may
1829
+ `apigateway:POST` on `arn:aws:apigateway:eu-west-2::/restapis/a1b2c3d4e5/resources`. Those ARNs
1830
+ carry no account id, because API Gateway control-plane ARNs leave that segment empty.
1831
+
1832
+ A policy naming an action such as `apigateway:CreateResource` matches nothing here, and it matches
1833
+ nothing on real AWS.
1834
+
1835
+ ## What is refused
1836
+
1837
+ An input outside what this simulates is refused. Dropping it would let a request look applied here
1838
+ and behave differently deployed. The refusals worth knowing about:
1839
+
1840
+ - **Authorizer kinds.** `CreateAuthorizer` takes `TOKEN`, `REQUEST` and `COGNITO_USER_POOLS`, which
1841
+ is every kind a REST API has. The `JWT` authorizer an HTTP API takes is the v2 service's and is
1842
+ refused here. An `AWS_IAM` method is decided by IAM and names no authorizer. See
1843
+ [Protecting a method with IAM](#protecting-a-method-with-iam).
1844
+ - **Identity source expressions.** A `REQUEST` authorizer reads
1845
+ `method.request.header.<name>` and `method.request.querystring.<name>`. AWS also allows
1846
+ `method.request.path`, `context` and `stageVariables`, and each is refused by
1847
+ `CreateAuthorizer`.
1848
+ - **Scopes on a method that checks none.** `authorizationScopes` is refused on a method that is not
1849
+ `COGNITO_USER_POOLS`, since a method carrying scopes nothing checks reads as gated by them.
1850
+ - **Holding a Cognito authorizer's decision.** `authorizerResultTtlInSeconds` is refused on a
1851
+ `COGNITO_USER_POOLS` authorizer. Real API Gateway holds that decision, and a token expiring
1852
+ during the period would still be accepted. A Lambda authorizer takes the member. See
1853
+ [Caching the authorizer's decision](#caching-the-authorizers-decision).
1854
+ - **The default period.** A Lambda authorizer written with no `authorizerResultTtlInSeconds` holds
1855
+ no decision here, where real API Gateway would hold one for 300 seconds. An authorizer counting
1856
+ its own invocations counts one per request until the member is written out.
1857
+ - **Integration types.** Only `AWS_PROXY` with a Lambda function URI is simulated. `MOCK`, `HTTP`,
1858
+ `HTTP_PROXY` and the non-proxy `AWS` type each answer a request from somewhere this cannot reach.
1859
+ - **API keys and usage plans.** `apiKeyRequired: true` is refused, because a method requiring a key
1860
+ here would answer requests real AWS rejects. A usage plan holds a per-key quota and is its own
1861
+ feature. Stage throttling is simulated, and is a bucket shared by every client. See
1862
+ [Throttling a stage and a method](#throttling-a-stage-and-a-method).
1863
+ - **Method settings outside throttling.** `cachingEnabled`, `metricsEnabled`, `loggingLevel` and
1864
+ `dataTraceEnabled` are refused by `CreateStage`. A template carrying one deploys, and the member it
1865
+ named is recorded on `stack.ignoredProperties`.
1866
+ - **Paging.** Every list command answers in full, and `limit` or `position` is refused.
1867
+ - **Endpoint types, request validators, models, mapping templates and WAF.** All refused.
1868
+ - **Updates.** `UpdateRestApi` replaces `/name` and `/description`. Any other patch path is refused.
1869
+ - **OpenAPI extensions.** An import reads `x-amazon-apigateway-integration`,
1870
+ `x-amazon-apigateway-any-method`, `x-amazon-apigateway-authtype`,
1871
+ `x-amazon-apigateway-authorizer` and `x-amazon-apigateway-auth`. Every other
1872
+ `x-amazon-apigateway-*` extension is refused, and so is any integration member beyond `type`,
1873
+ `httpMethod` and `uri`. Each refusal names the JSON pointer of the member.
1874
+
1875
+ ## Available functionality
1876
+
1877
+ | Area | Commands |
1878
+ | ------------ | ------------------------------------------------------------------------------ |
1879
+ | REST APIs | `CreateRestApi`, `GetRestApi`, `GetRestApis`, `UpdateRestApi`, `DeleteRestApi` |
1880
+ | OpenAPI | `ImportRestApi`, `PutRestApi` |
1881
+ | Resources | `CreateResource`, `GetResource`, `GetResources`, `DeleteResource` |
1882
+ | Authorizers | `CreateAuthorizer`, `GetAuthorizer`, `GetAuthorizers`, `DeleteAuthorizer` |
1883
+ | Methods | `PutMethod`, `GetMethod`, `DeleteMethod` |
1884
+ | Integrations | `PutIntegration`, `GetIntegration` |
1885
+ | Publishing | `CreateDeployment`, `CreateStage`, `GetStage`, `GetStages`, `DeleteStage` |
1886
+
1887
+ A stage can be protected by a simulated WAFv2 web ACL. See
1888
+ [Protecting a stage with a web ACL](#protecting-a-stage-with-a-web-acl).
1889
+
1890
+ CloudFormation deploys `AWS::ApiGateway::RestApi`, `Resource`, `Method`, `Authorizer`, `Deployment`
1891
+ and `Stage`, including the template CDK synthesizes from a `RestApi` or a `LambdaRestApi`. See
1892
+ [Deploying from CloudFormation and CDK](#deploying-from-cloudformation-and-cdk).
1893
+
1894
+ ## Limitations
1895
+
1896
+ - An `AWS::ApiGateway::Deployment` is created and never deleted, because API Gateway deletes one
1897
+ and this simulation has no command for it. A Stack teardown lists it in
1898
+ `stack.skippedResourceDeletions` and the deployment goes with its API a moment later.
1899
+ - A repeated request header reaches the handler as one joined value in `multiValueHeaders`, because
1900
+ that is the form the platform's `Headers` hands over. Real API Gateway reports each separately.
1901
+ - A Lambda authorizer's `context` reaches the handler as the authorizer returned it. AWS accepts a
1902
+ string, a number or a boolean for each value, and how it renders them is not published.
1903
+ - A Cognito authorizer reads a `providerARN` for the pool id it names, so a pool in another account
1904
+ is verified against whenever this simulation holds it. `identityValidationExpression`, which real
1905
+ API Gateway matches a token against before verifying it, is outside this.
1906
+ - Binary media types negotiated by `Accept`, CORS preflight and gateway responses are outside this.
1907
+ A response body is still base64 decoded when the handler says `isBase64Encoded`.
1908
+ - The account-level rate and burst limits are outside this. A stage that names no limit throttles
1909
+ nothing, and a method setting naming one limit alone leaves that method unthrottled.
1910
+ - WebSocket APIs are outside this and outside the v2 service.
1911
+ - Only OpenAPI 3.0.x is imported. `ImportRestApi` and `PutRestApi` refuse a `swagger: "2.0"`
1912
+ document and an `openapi: "3.1.0"` one by version. A `Body` carrying a Swagger 2.0 document is
1913
+ recorded and the API deploys without it. The body is JSON, and YAML is refused with the same
1914
+ message.
1915
+ - A security scheme declares a Lambda authorizer, a Cognito one or IAM authorization. An `apiKey`
1916
+ scheme carrying no `x-amazon-apigateway-authtype` is an API key and is refused, and so are the
1917
+ `http`, `oauth2` and `openIdConnect` scheme types. See [Security schemes](#security-schemes).
1918
+ - `PutRestApi` replaces a definition and never merges one. `BodyS3Location` on the Resource, and a
1919
+ document held anywhere but inline, are outside this.