@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,1283 @@
1
+ # Simulated IAM
2
+
3
+ Yulin includes a simulated IAM service for tests and local development.
4
+
5
+ Sim IAM stores simulated Roles, Users and Policies, and evaluates allow/deny authorization decisions
6
+ for them. Other simulated services use it to authorize their own actions, simulated STS uses it to
7
+ issue temporary Role sessions, and sim CloudFormation can create IAM resources from templates. It can
8
+ also be instantiated on its own as `SimIam` with isolated state.
9
+
10
+ ## Basic usage
11
+
12
+ Create a simulated AWS environment, get simulated IAM, create a Role with an inline policy, and
13
+ authorize an action as that Role.
14
+
15
+ ```typescript sim-iam-role-authorization
16
+ /**
17
+ * Creating a simulated IAM Role and authorizing an action.
18
+ */
19
+
20
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
21
+ import { SimAws } from "@kensio/yulin";
22
+
23
+ const simAws = new SimAws();
24
+ const simIam = simAws.account("123456789012").iam();
25
+
26
+ const roleCreation = await simIam.createRole(
27
+ new CreateRoleCommand({
28
+ RoleName: "ReportReaderRole",
29
+ Description: "Allows reading report objects",
30
+ AssumeRolePolicyDocument: JSON.stringify({
31
+ Version: "2012-10-17",
32
+ Statement: {
33
+ Effect: "Allow",
34
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
35
+ Action: "sts:AssumeRole",
36
+ },
37
+ }),
38
+ }),
39
+ );
40
+
41
+ await simIam.putRolePolicy(
42
+ new PutRolePolicyCommand({
43
+ RoleName: "ReportReaderRole",
44
+ PolicyName: "ReadReports",
45
+ PolicyDocument: JSON.stringify({
46
+ Version: "2012-10-17",
47
+ Statement: {
48
+ Effect: "Allow",
49
+ Action: "s3:GetObject",
50
+ Resource: "arn:aws:s3:::reports-bucket/*",
51
+ },
52
+ }),
53
+ }),
54
+ );
55
+
56
+ const decision = simIam.authorize({
57
+ action: "s3:GetObject",
58
+ resource: "arn:aws:s3:::reports-bucket/2026/summary.csv",
59
+ caller: { kind: "arn", arn: roleCreation.Role.Arn },
60
+ });
61
+
62
+ console.log(decision.isAllowed);
63
+ ```
64
+
65
+ `CreateRoleCommand` validates the trust policy document and stores the Role with an AWS-shaped ARN,
66
+ Role ID, and creation date. Roles can be inspected with `GetRoleCommand` and `ListRolesCommand`.
67
+
68
+ A trust policy alone grants no permissions. A Role with no inline or attached policies is implicitly
69
+ denied for every action.
70
+
71
+ ## Authorization decisions
72
+
73
+ `authorize(...)` returns a decision object. A denied request comes back as a decision too, and a
74
+ test can assert on exactly why it was allowed or denied. The decision models the common IAM
75
+ evaluation rules:
76
+
77
+ - A matching explicit `Deny` statement in any evaluated policy wins
78
+ - Otherwise, within one Account, a matching `Allow` in an identity policy or resource policy allows
79
+ the request
80
+ - Across Accounts, a matching `Allow` is needed from each side. See
81
+ [Cross-Account requests](#cross-account-requests)
82
+ - Otherwise the request is implicitly denied
83
+
84
+ The decision exposes `value` (`"Allow"`, `"ExplicitDeny"`, or `"ImplicitDeny"`), the convenience
85
+ flags `isAllowed`, `isDenied`, `isExplicitDeny`, and `isImplicitDeny`, the matching
86
+ `allowStatements` and `explicitDenyStatements`, and the resolved `caller` for diagnostics. The
87
+ matching Allows are also available per side as `identityAllowStatements` and
88
+ `resourceAllowStatements`. A cross-Account denial is best read from those. Statements the simulator
89
+ could not evaluate are reported by `unevaluatedStatements`, covered under
90
+ [Statements left unevaluated](#statements-left-unevaluated).
91
+
92
+ If the caller is omitted, authorization defaults to the simulation's own
93
+ [default caller](#name-the-caller-a-simulation-uses-by-default), and to the root principal of the
94
+ Account owning the sim IAM instance where the simulation has none. That root principal is allowed
95
+ within its own Account, subject to any service control policy over that Account. An explicit
96
+ `{ kind: "anonymous" }` caller suppresses both fallbacks and is
97
+ evaluated without identity policies.
98
+
99
+ Resource policies live with the service that owns the target resource, such as an S3 Bucket policy,
100
+ and are supplied with the authorization request.
101
+
102
+ ```typescript sim-iam-authorization-decisions
103
+ /**
104
+ * Inspecting simulated IAM authorization decisions.
105
+ */
106
+
107
+ import { SimAws } from "@kensio/yulin";
108
+
109
+ const simAws = new SimAws();
110
+ const simIam = simAws.account("123456789012").iam();
111
+
112
+ const bucketPolicy = {
113
+ document: {
114
+ Version: "2012-10-17",
115
+ Statement: [
116
+ {
117
+ Effect: "Allow",
118
+ Principal: "*",
119
+ Action: "s3:GetObject",
120
+ Resource: "arn:aws:s3:::example-bucket/*",
121
+ },
122
+ {
123
+ Effect: "Deny",
124
+ Principal: "*",
125
+ Action: "s3:GetObject",
126
+ Resource: "arn:aws:s3:::example-bucket/private/*",
127
+ },
128
+ ],
129
+ },
130
+ } as const;
131
+
132
+ const publicDecision = simIam.authorize({
133
+ action: "s3:GetObject",
134
+ resource: "arn:aws:s3:::example-bucket/public/index.html",
135
+ caller: { kind: "anonymous" },
136
+ resourcePolicies: [bucketPolicy],
137
+ });
138
+
139
+ const privateDecision = simIam.authorize({
140
+ action: "s3:GetObject",
141
+ resource: "arn:aws:s3:::example-bucket/private/secrets.txt",
142
+ caller: { kind: "anonymous" },
143
+ resourcePolicies: [bucketPolicy],
144
+ });
145
+
146
+ console.log(publicDecision.value);
147
+ console.log(privateDecision.value);
148
+ console.log(privateDecision.explicitDenyStatements.length);
149
+ ```
150
+
151
+ ## Managed Policies
152
+
153
+ Create standalone managed Policies with `CreatePolicyCommand` and attach them to Roles with
154
+ `AttachRolePolicyCommand`. A managed Policy only grants permissions once it is attached.
155
+
156
+ ```typescript sim-iam-managed-policy
157
+ /**
158
+ * Creating and attaching a simulated IAM managed Policy.
159
+ */
160
+
161
+ import {
162
+ AttachRolePolicyCommand,
163
+ CreatePolicyCommand,
164
+ CreateRoleCommand,
165
+ } from "@aws-sdk/client-iam";
166
+ import { SimAws } from "@kensio/yulin";
167
+
168
+ const simAws = new SimAws();
169
+ const simIam = simAws.account("123456789012").iam();
170
+
171
+ const policyCreation = await simIam.createPolicy(
172
+ new CreatePolicyCommand({
173
+ PolicyName: "ReadOnlyReports",
174
+ Path: "/service-role/",
175
+ PolicyDocument: JSON.stringify({
176
+ Version: "2012-10-17",
177
+ Statement: {
178
+ Effect: "Allow",
179
+ Action: "s3:GetObject",
180
+ Resource: "arn:aws:s3:::reports-bucket/*",
181
+ },
182
+ }),
183
+ }),
184
+ );
185
+
186
+ const roleCreation = await simIam.createRole(
187
+ new CreateRoleCommand({
188
+ RoleName: "ReportingRole",
189
+ AssumeRolePolicyDocument: JSON.stringify({
190
+ Version: "2012-10-17",
191
+ Statement: {
192
+ Effect: "Allow",
193
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
194
+ Action: "sts:AssumeRole",
195
+ },
196
+ }),
197
+ }),
198
+ );
199
+
200
+ await simIam.attachRolePolicy(
201
+ new AttachRolePolicyCommand({
202
+ RoleName: "ReportingRole",
203
+ PolicyArn: policyCreation.Policy.Arn,
204
+ }),
205
+ );
206
+
207
+ const decision = simIam.authorize({
208
+ action: "s3:GetObject",
209
+ resource: "arn:aws:s3:::reports-bucket/2026/summary.csv",
210
+ caller: { kind: "arn", arn: roleCreation.Role.Arn },
211
+ });
212
+
213
+ console.log(policyCreation.Policy.Arn);
214
+ console.log(decision.isAllowed);
215
+ ```
216
+
217
+ Policy paths are normalised into the Policy ARN. A Policy named `ReadOnlyReports` with path
218
+ `/service-role/` gets the ARN `arn:aws:iam::123456789012:policy/service-role/ReadOnlyReports`.
219
+ Creating a duplicate Policy name in the same path throws an error, while the same name in different
220
+ paths is allowed. Stored Policies can be inspected with `GetPolicyCommand` and
221
+ `ListPoliciesCommand`.
222
+
223
+ ## Policy conditions
224
+
225
+ Policy statements can carry `Condition` blocks. Sim IAM currently supports the `StringEquals`,
226
+ `StringLike`, `ArnLike`, `ArnEquals` and `NumericLessThanEquals` operators, along with the
227
+ `ForAllValues:` and `ForAnyValue:` set variants of `StringEquals` and `StringLike`.
228
+
229
+ The negated operators `StringNotEquals`, `StringNotLike`, `ArnNotEquals` and `ArnNotLike` are
230
+ supported too, each unqualified and in both set forms. A list of policy values under a negated
231
+ operator is an AND (the request value has to differ from every one of them), where the same list
232
+ under a positive operator is an OR. A service control policy writes its carve-outs this way, hanging
233
+ `ArnNotLike` on `aws:PrincipalArn` to deny an action to every principal outside a named set of
234
+ roles.
235
+
236
+ `ArnLike` and `ArnEquals` behave identically, as AWS documents them doing. Both compare the six
237
+ colon-delimited components of an ARN separately, and both accept `*` and `?` wildcards in any of
238
+ them. A wildcard stays inside the component it is written in. `arn:aws:s3:*` matches nothing,
239
+ because a pattern needs as many components as the ARN it is matched against. `ArnNotEquals` and
240
+ `ArnNotLike` compare an ARN the same way and answer the opposite.
241
+
242
+ Condition context values are supplied by the service handling the simulated request, such as S3
243
+ object tags. Sim IAM automatically derives the global values it can work out itself, `aws:PrincipalArn`
244
+ from the resolved caller and `aws:RequestedRegion` from the Region the request was made in. A value a
245
+ service supplies under either name is overwritten by the derived one. Context-key names are matched
246
+ case-insensitively, while string values remain case-sensitive.
247
+
248
+ Every simulated service supplies its own Region. A policy conditioned on `aws:RequestedRegion`
249
+ therefore sees the Region of the service that handled the request, whichever Region the caller was
250
+ in, and a CloudFormation deployment carries the Region of the Stack it is deploying. IAM, CloudFront
251
+ and Route53 are global, with one endpoint between all Regions. Their requests carry `us-east-1`, as
252
+ they do on AWS (which is why a service control policy confining an Account to a list of Regions has
253
+ to leave the global services' actions out of the condition). A request made straight to
254
+ `simIam.authorize(...)` carries no Region unless it is given one, and a statement conditioned on the
255
+ key then matches nothing.
256
+
257
+ ```typescript sim-iam-policy-conditions
258
+ /**
259
+ * Simulated IAM policy conditions.
260
+ */
261
+
262
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
263
+ import { SimAws } from "@kensio/yulin";
264
+
265
+ const simAws = new SimAws();
266
+ const simIam = simAws.account("123456789012").iam();
267
+
268
+ const roleCreation = await simIam.createRole(
269
+ new CreateRoleCommand({
270
+ RoleName: "FinanceReaderRole",
271
+ AssumeRolePolicyDocument: JSON.stringify({
272
+ Version: "2012-10-17",
273
+ Statement: {
274
+ Effect: "Allow",
275
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
276
+ Action: "sts:AssumeRole",
277
+ },
278
+ }),
279
+ }),
280
+ );
281
+
282
+ await simIam.putRolePolicy(
283
+ new PutRolePolicyCommand({
284
+ RoleName: "FinanceReaderRole",
285
+ PolicyName: "ReadFinanceObjects",
286
+ PolicyDocument: JSON.stringify({
287
+ Version: "2012-10-17",
288
+ Statement: {
289
+ Effect: "Allow",
290
+ Action: "s3:GetObject",
291
+ Resource: "arn:aws:s3:::reports-bucket/*",
292
+ Condition: {
293
+ StringEquals: {
294
+ "s3:ExistingObjectTag/department": "finance",
295
+ },
296
+ },
297
+ },
298
+ }),
299
+ }),
300
+ );
301
+
302
+ const decision = simIam.authorize({
303
+ action: "s3:GetObject",
304
+ resource: "arn:aws:s3:::reports-bucket/2026/summary.csv",
305
+ caller: { kind: "arn", arn: roleCreation.Role.Arn },
306
+ conditionContext: {
307
+ "s3:ExistingObjectTag/department": "finance",
308
+ },
309
+ });
310
+
311
+ console.log(decision.isAllowed);
312
+ ```
313
+
314
+ An unqualified positive operator naming a context key the request supplies no value for fails to
315
+ match, leaving the request implicitly denied unless another statement allows it. Its negated form
316
+ matches instead, as AWS documents. With no value in the request there is none for the policy value
317
+ to equal. A `ForAnyValue:` operator answers false for an absent key whatever it wraps, because no
318
+ request value is there to satisfy it.
319
+
320
+ ### Statements left unevaluated
321
+
322
+ An operator from outside the list above fails closed. The statement holding it matches nothing, and
323
+ a `Deny` written that way stops nothing. The request then goes through on whatever else allows it,
324
+ which is how a guardrail comes to report a healthy Allow. `decision.unevaluatedStatements` reports
325
+ each of those statements, with the policy it came from (`policy`), how that policy reached the
326
+ request (`sourceType`), the statement as its document declared it (`statement`), and the operator
327
+ the simulator could not evaluate (`reason`).
328
+
329
+ Every operator in a condition block is read, and an unsupported one leaves the rest of the block
330
+ evaluated as usual. A statement reaches the list once everything else about it has matched. Its
331
+ Principal, Action and Resource applied to the request, and the unsupported operator was the only
332
+ thing standing between the statement and the request. A decision reached over policies the
333
+ simulator read in full reports an empty list, and a test asserting on a guardrail can say so.
334
+
335
+ ```typescript sim-iam-unevaluated-statements
336
+ /**
337
+ * Reporting simulated IAM statements that could not be evaluated.
338
+ */
339
+
340
+ import { SimAws } from "@kensio/yulin";
341
+
342
+ const simAws = new SimAws({ defaultAccountId: "123456789012" });
343
+
344
+ simAws.organizations().attachServiceControlPolicy(
345
+ "123456789012",
346
+ {
347
+ Version: "2012-10-17",
348
+ Statement: {
349
+ Sid: "DenyBucketCreationAfterFreeze",
350
+ Effect: "Deny",
351
+ Action: "s3:CreateBucket",
352
+ Resource: "*",
353
+ Condition: {
354
+ DateGreaterThan: { "aws:CurrentTime": "2026-01-01T00:00:00Z" },
355
+ },
356
+ },
357
+ },
358
+ { policyName: "BucketGuardrail" },
359
+ );
360
+
361
+ const decision = simAws.account("123456789012").iam().authorize({
362
+ action: "s3:CreateBucket",
363
+ resource: "arn:aws:s3:::123456789012-reports",
364
+ });
365
+
366
+ const [unevaluated] = decision.unevaluatedStatements;
367
+
368
+ console.log(decision.isAllowed); // true
369
+ console.log(unevaluated?.policy); // "BucketGuardrail"
370
+ console.log(unevaluated?.reason); // "unsupported condition operator DateGreaterThan"
371
+ ```
372
+
373
+ ## Users and access keys
374
+
375
+ Create Users with `CreateUserCommand`, give them inline policies with `PutUserPolicyCommand`, and
376
+ issue access keys with `CreateAccessKeyCommand`. Access keys are registered with the Account's
377
+ credential registry. Credentials can then be supplied as the caller of an authorization attempt,
378
+ and are authenticated before policy evaluation.
379
+
380
+ `DeleteUserCommand` removes a User. IAM refuses it while the User still holds an inline policy or an
381
+ attached managed policy, the way `DeleteRoleCommand` refuses a Role, and answers `NoSuchEntity` for
382
+ a name the Account does not hold. Real IAM also refuses a User that still has access keys or a login
383
+ profile. Sim IAM serves no way to remove either, and lets the User go.
384
+
385
+ ```typescript sim-iam-user-access-key
386
+ /**
387
+ * Simulated IAM Users, inline policies, and access keys.
388
+ */
389
+
390
+ import {
391
+ CreateAccessKeyCommand,
392
+ CreateUserCommand,
393
+ PutUserPolicyCommand,
394
+ } from "@aws-sdk/client-iam";
395
+ import { SimAws } from "@kensio/yulin";
396
+
397
+ const simAws = new SimAws();
398
+ const simIam = simAws.account("123456789012").iam();
399
+
400
+ await simIam.createUser(
401
+ new CreateUserCommand({
402
+ UserName: "ApplicationUser",
403
+ Path: "/application/",
404
+ }),
405
+ );
406
+
407
+ await simIam.putUserPolicy(
408
+ new PutUserPolicyCommand({
409
+ UserName: "ApplicationUser",
410
+ PolicyName: "ReadAssets",
411
+ PolicyDocument: JSON.stringify({
412
+ Version: "2012-10-17",
413
+ Statement: {
414
+ Effect: "Allow",
415
+ Action: "s3:GetObject",
416
+ Resource: "arn:aws:s3:::assets-bucket/*",
417
+ },
418
+ }),
419
+ }),
420
+ );
421
+
422
+ const accessKeyCreation = await simIam.createAccessKey(
423
+ new CreateAccessKeyCommand({
424
+ UserName: "ApplicationUser",
425
+ }),
426
+ );
427
+
428
+ const decision = simIam.authorize({
429
+ action: "s3:GetObject",
430
+ resource: "arn:aws:s3:::assets-bucket/images/logo.svg",
431
+ caller: {
432
+ kind: "credentials",
433
+ credentials: {
434
+ accessKeyId: accessKeyCreation.AccessKey.AccessKeyId,
435
+ secretAccessKey: accessKeyCreation.AccessKey.SecretAccessKey,
436
+ },
437
+ },
438
+ });
439
+
440
+ console.log(decision.isAllowed);
441
+ console.log(decision.caller.arn);
442
+ ```
443
+
444
+ Invalid credentials throw an AWS-like error before any policies are evaluated, with a diagnostic
445
+ reason such as an unknown access key, a secret access key mismatch, or an expired session.
446
+
447
+ `AttachUserPolicyCommand` attaches a managed Policy to a User by ARN, the way
448
+ `AttachRolePolicyCommand` does for a Role. An ARN with no stored Policy behind it, such as an
449
+ AWS-managed one, attaches and contributes no statements to a decision.
450
+
451
+ `CreateLoginProfileCommand` gives a User a console password. The response describes the profile
452
+ without the password, which is how real IAM behaves. See
453
+ [CloudFormation Users](#users) for reading the password back out of the simulator.
454
+
455
+ ## STS AssumeRole sessions
456
+
457
+ Simulated STS issues temporary credentials for IAM Roles with `AssumeRoleCommand`. The assume
458
+ request is evaluated against the Role's trust policy, and the returned credentials resolve to an
459
+ assumed-role session principal whose permissions come from the underlying Role's policies.
460
+
461
+ A session carries two ARNs, the session's own and the Role's, and a resource policy naming either
462
+ one applies to it. A Bucket policy naming a Role therefore covers every session of that Role, as it
463
+ does in AWS, and `aws:PrincipalArn` holds the Role's ARN throughout.
464
+
465
+ ```typescript sim-iam-sts-assume-role
466
+ /**
467
+ * Assuming a simulated IAM Role through simulated STS.
468
+ */
469
+
470
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
471
+ import { AssumeRoleCommand } from "@aws-sdk/client-sts";
472
+ import { SimAws } from "@kensio/yulin";
473
+
474
+ const simAws = new SimAws();
475
+ const account = simAws.account("123456789012");
476
+ const simIam = account.iam();
477
+
478
+ await simIam.createRole(
479
+ new CreateRoleCommand({
480
+ RoleName: "DeploymentRole",
481
+ AssumeRolePolicyDocument: JSON.stringify({
482
+ Version: "2012-10-17",
483
+ Statement: {
484
+ Effect: "Allow",
485
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
486
+ Action: "sts:AssumeRole",
487
+ },
488
+ }),
489
+ }),
490
+ );
491
+
492
+ await simIam.putRolePolicy(
493
+ new PutRolePolicyCommand({
494
+ RoleName: "DeploymentRole",
495
+ PolicyName: "PutDeploymentObjects",
496
+ PolicyDocument: JSON.stringify({
497
+ Version: "2012-10-17",
498
+ Statement: {
499
+ Effect: "Allow",
500
+ Action: "s3:PutObject",
501
+ Resource: "arn:aws:s3:::deployments-bucket/*",
502
+ },
503
+ }),
504
+ }),
505
+ );
506
+
507
+ const assumeRoleOutput = await account.sts().assumeRole(
508
+ new AssumeRoleCommand({
509
+ RoleArn: "arn:aws:iam::123456789012:role/DeploymentRole",
510
+ RoleSessionName: "deploy-session",
511
+ }),
512
+ );
513
+
514
+ const credentials = assumeRoleOutput.Credentials!;
515
+
516
+ const decision = simIam.authorize({
517
+ action: "s3:PutObject",
518
+ resource: "arn:aws:s3:::deployments-bucket/release.zip",
519
+ caller: {
520
+ kind: "credentials",
521
+ credentials: {
522
+ accessKeyId: credentials.AccessKeyId!,
523
+ secretAccessKey: credentials.SecretAccessKey!,
524
+ sessionToken: credentials.SessionToken!,
525
+ },
526
+ },
527
+ });
528
+
529
+ console.log(decision.isAllowed);
530
+ console.log(decision.caller.arn);
531
+ ```
532
+
533
+ The resolved caller ARN is the STS assumed-role session ARN, such as
534
+ `arn:aws:sts::123456789012:assumed-role/DeploymentRole/deploy-session`, while identity policies and
535
+ the derived `aws:PrincipalArn` come from the underlying Role. A caller that the trust policy does
536
+ not allow is denied the assume request, session credentials require their session token, and
537
+ expired sessions are rejected.
538
+
539
+ ## Name the caller a simulation uses by default
540
+
541
+ Every simulated operation takes a `caller`, and a call that gives none is decided as the root
542
+ principal of the Account it reaches. `defaultCaller` on `SimAws` names a principal for those calls,
543
+ such as the Role an operator would be reading the account through.
544
+
545
+ ```typescript sim-iam-default-caller
546
+ /**
547
+ * Naming who a call that states no caller comes from.
548
+ */
549
+
550
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
551
+ import { SimAws } from "@kensio/yulin";
552
+
553
+ const simAws = new SimAws({
554
+ defaultAccountId: "123456789012",
555
+ defaultCaller: {
556
+ kind: "arn",
557
+ arn: "arn:aws:iam::123456789012:role/Administrator",
558
+ },
559
+ });
560
+
561
+ // The Role is created as the Account root. A simulation with a default caller
562
+ // attributes these commands to it, and it holds no policy until they are done.
563
+ const root = simAws.account().rootPrincipal;
564
+ const simIam = simAws.iam();
565
+
566
+ await simIam.createRole(
567
+ new CreateRoleCommand({
568
+ RoleName: "Administrator",
569
+ AssumeRolePolicyDocument: JSON.stringify({
570
+ Version: "2012-10-17",
571
+ Statement: {
572
+ Effect: "Allow",
573
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
574
+ Action: "sts:AssumeRole",
575
+ },
576
+ }),
577
+ }),
578
+ { caller: root },
579
+ );
580
+
581
+ await simIam.putRolePolicy(
582
+ new PutRolePolicyCommand({
583
+ RoleName: "Administrator",
584
+ PolicyName: "Administer",
585
+ PolicyDocument: JSON.stringify({
586
+ Version: "2012-10-17",
587
+ Statement: { Effect: "Allow", Action: "*", Resource: "*" },
588
+ }),
589
+ }),
590
+ { caller: root },
591
+ );
592
+
593
+ const decision = simAws.iam().authorize({
594
+ action: "s3:GetObject",
595
+ resource: "arn:aws:s3:::reports-bucket/summary.csv",
596
+ });
597
+
598
+ console.log(decision.caller.arn); // "arn:aws:iam::123456789012:role/Administrator"
599
+ console.log(decision.isAllowed); // true
600
+ ```
601
+
602
+ `defaultCaller` takes a principal or a caller resolved elsewhere, such as an assumed-role session
603
+ carrying the Role its policies come from. Credentials are the one caller it refuses. Only the
604
+ Account that issued a key can authenticate it, and a default is held for a whole simulation.
605
+
606
+ The default reaches a direct sim service call, an intercepted SDK Command, a CloudFormation
607
+ deployment and STS. Three things outrank it, each of them a caller stated for the request in hand.
608
+ An operation's own `caller` wins, as does the ambient caller of a `runAs` block and the `caller` a
609
+ deployment is given.
610
+
611
+ A simulation told no `defaultCaller` decides an unattributed call as the Account root, which is what
612
+ every simulation did before the option existed. The root keeps the identity access sim IAM gives it
613
+ either way, and a test about root behaviour reaches it as `simAws.account().rootPrincipal`. A
614
+ service control policy denying that root still overrides the access.
615
+
616
+ The reason to name one is a service control policy denying the Account root.
617
+ [Simulated Organizations](https://yulinsim.dev/services/organizations/) covers that case, where
618
+ every unattributed read is otherwise denied on a message about the root.
619
+
620
+ ## Callers of HTTP requests
621
+
622
+ An in-process SDK call can be told who its caller is. A request arriving over HTTP, through
623
+ `serveSimAws` or through `SimAwsHttp.fetch(...)` in the same process, carries no such thing. Sim IAM
624
+ works the caller out from the request itself, in a fixed order:
625
+
626
+ 1. An `x-sim-aws-caller` header naming the principal directly.
627
+ 2. An `Authorization: AWS4-HMAC-SHA256` header, verified as a SigV4 signature.
628
+ 3. Failing both, **anonymous**.
629
+
630
+ Sim IAM treats an omitted in-process caller as the Account root with unrestricted access, a
631
+ convenience inside a test. Over HTTP the same default would make every unauthenticated request an
632
+ administrator. A served request that says nothing about who sent it is anonymous.
633
+
634
+ ### Naming the caller directly
635
+
636
+ `x-sim-aws-caller` names the principal outright. It is the path for local development and for
637
+ tooling that will not sign requests. A curl one-liner can be a Role without holding any credentials.
638
+
639
+ ```bash
640
+ curl -H 'x-sim-aws-caller: arn:aws:iam::111111111111:role/Reporter' \
641
+ http://abc123.lambda-url.us-east-1.sim-aws.localhost:4566/
642
+ ```
643
+
644
+ The value is one of three forms:
645
+
646
+ | Value | Principal |
647
+ | ---------------- | ------------------------------------------------------------ |
648
+ | An ARN | That IAM User, Role, or assumed-role session |
649
+ | `service:<name>` | An AWS service principal, such as `service:s3.amazonaws.com` |
650
+ | `anonymous` | Explicitly anonymous |
651
+
652
+ The header is always enabled and not configurable, and it takes precedence over a valid signature.
653
+ The ARN it names is taken as given, since naming a principal is a separate thing from claiming it
654
+ was created, exactly as `runAs` behaves. The header is stripped before the request reaches the
655
+ simulated service. A Lambda handler echoing `event.headers` sees none of the simulator's control
656
+ metadata.
657
+
658
+ ```typescript sim-iam-served-request-caller
659
+ /**
660
+ * Naming the caller of an HTTP request into simulated AWS.
661
+ */
662
+
663
+ import {
664
+ CreateFunctionCommand,
665
+ CreateFunctionUrlConfigCommand,
666
+ } from "@aws-sdk/client-lambda";
667
+
668
+ import { SimAws } from "@kensio/yulin";
669
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
670
+ import { serveSimAws } from "@kensio/yulin/serve";
671
+
672
+ const simAws = new SimAws();
673
+
674
+ await simAws.lambda().createFunction(
675
+ new CreateFunctionCommand({
676
+ FunctionName: "reporter",
677
+ Role: "arn:aws:iam::111111111111:role/ReporterRole",
678
+ Code: { ZipFile: makeLambdaZipFileInput(() => ({ ok: true })) },
679
+ }),
680
+ );
681
+
682
+ const urlConfig = await simAws.lambda().createFunctionUrlConfig(
683
+ new CreateFunctionUrlConfigCommand({
684
+ FunctionName: "reporter",
685
+ AuthType: "NONE",
686
+ }),
687
+ );
688
+
689
+ const srv = await serveSimAws({ simAws });
690
+
691
+ try {
692
+ const response = await fetch(srv.localUrl(urlConfig.FunctionUrl), {
693
+ headers: { "x-sim-aws-caller": "arn:aws:iam::111111111111:role/Reporter" },
694
+ });
695
+
696
+ // arn:aws:iam::111111111111:role/Reporter
697
+ console.log(response.headers.get("x-sim-aws-caller"));
698
+ // caller-header
699
+ console.log(response.headers.get("x-sim-aws-auth"));
700
+ } finally {
701
+ await srv.close();
702
+ }
703
+ ```
704
+
705
+ ### Naming what the request is for
706
+
707
+ When one AWS service calls another it says which of your resources it is calling for, and IAM
708
+ supplies that as `aws:SourceArn` and `aws:SourceAccount`. A resource policy granting a service
709
+ principal is usually conditioned on them, so that a Bucket policy or a function's resource policy
710
+ admits one Distribution rather than every CloudFront customer. Two more headers say the same thing
711
+ over HTTP:
712
+
713
+ | Header | Condition key |
714
+ | -------------------------- | ------------------- |
715
+ | `x-sim-aws-source-arn` | `aws:SourceArn` |
716
+ | `x-sim-aws-source-account` | `aws:SourceAccount` |
717
+
718
+ ```bash
719
+ curl -H 'x-sim-aws-caller: service:cloudfront.amazonaws.com' \
720
+ -H 'x-sim-aws-source-arn: arn:aws:cloudfront::111111111111:distribution/E1EXAMPLE12345' \
721
+ http://abc123.lambda-url.us-east-1.sim-aws.localhost:4566/
722
+ ```
723
+
724
+ A request that supplies neither header leaves both condition keys unset, and an unset key is a
725
+ different thing from an empty one. A statement conditioned on either key fails to match. Both
726
+ headers are stripped before the request reaches the simulated service, as the caller header is.
727
+
728
+ Sim CloudFront sends these itself when a Distribution reaches a custom Origin through an origin
729
+ access control. That is how an `AWS_IAM` Lambda Function URL behind CloudFront is admitted.
730
+ Simulated Lambda Function URLs are the only endpoint evaluating them so far.
731
+
732
+ ### Signed requests
733
+
734
+ A request signed with credentials from `CreateAccessKeyCommand` or from an STS `AssumeRoleCommand`
735
+ session is verified as a SigV4 signature and resolves to the signing principal, with the same
736
+ identity `resolveCredentials` returns in process. For an assumed-role session that means the
737
+ request is attributed to the session while its permissions come from the Role behind it. A policy
738
+ on the Role applies to a request the session signed.
739
+
740
+ Sign the URL you actually call. Serving rewrites AWS endpoint hostnames to local ones, and a
741
+ Function URL is served at `<url-id>.lambda-url.<region>.sim-aws.localhost:<port>`. The `host` header
742
+ is part of what a signature covers. A signature made against the real AWS hostname fails against the
743
+ local one.
744
+
745
+ A signature whose credential scope names a different service or Region than the endpoint it reached
746
+ is refused before anything else is checked, and says so. The scope feeds the signing key. Without
747
+ that check the only symptom would be a bare signature mismatch.
748
+
749
+ ### Presigned URLs
750
+
751
+ A URL carrying its signature in query parameters is verified the same way. `X-Amz-Algorithm` in the
752
+ query is what marks it, and the access key, credential scope, signed headers and signature all come
753
+ from the query string. A presigned URL also states its own lifetime in `X-Amz-Expires`, and that
754
+ _is_ enforced, against simulated time. A frozen clock keeps a URL usable, and advancing past the
755
+ window refuses it with `AccessDenied` and `Request has expired`.
756
+
757
+ URLs built by the real presigner, `getSignedUrl` from `@aws-sdk/s3-request-presigner`, verify here
758
+ without anything simulator-specific. See
759
+ [the sim S3 docs](https://yulinsim.dev/services/s3/#presigned-urls) for the whole path from presigning to fetching.
760
+
761
+ ### What the simulator reports back
762
+
763
+ Every served response carries the simulator's own account of the request in headers, leaving the
764
+ response body the shape the real service returns. Which headers appear depends on whether the
765
+ request was accepted:
766
+
767
+ | Header | On an accepted request | On a refused request |
768
+ | ------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------- |
769
+ | `x-sim-aws-caller` | The principal the request was attributed to, in the same form the request header accepts | Absent, as there is no principal to report |
770
+ | `x-sim-aws-auth` | How that was decided: `caller-header`, `sigv4`, or `none` | `rejected` |
771
+ | `x-sim-aws-error` | Absent | The AWS error code, such as `SignatureDoesNotMatch` |
772
+ | `x-sim-aws-error-detail` | Absent | What the simulator can say about why |
773
+
774
+ A refused request is answered as real AWS answers it. A rejected signature gets `403` with
775
+ `{"Message":"Forbidden"}`. A signature too incomplete to parse, or an `x-sim-aws-caller` value that
776
+ names no principal form, gets `400`. Real AWS has nowhere in that body to explain itself, and this
777
+ follows it. The detail goes in `x-sim-aws-error-detail`, out of the way of a client parsing the
778
+ response.
779
+
780
+ ## Authorizing other simulated services
781
+
782
+ Simulated services use sim IAM to authorize their own actions when used through `SimAws`. Route53
783
+ commands such as `CreateHostedZoneCommand`, `GetHostedZoneCommand`,
784
+ `ChangeResourceRecordSetsCommand`, `ListHostedZonesByNameCommand`, and
785
+ `ListResourceRecordSetsCommand` accept an optional caller, letting tests exercise real allow/deny
786
+ behaviour across services.
787
+
788
+ ```typescript sim-iam-route53-authorization
789
+ /**
790
+ * Simulated IAM authorization of Route53 actions.
791
+ */
792
+
793
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
794
+ import { CreateHostedZoneCommand } from "@aws-sdk/client-route-53";
795
+ import { SimAws } from "@kensio/yulin";
796
+
797
+ const simAws = new SimAws();
798
+ const account = simAws.account("123456789012");
799
+ const simIam = account.iam();
800
+ const simRoute53 = account.route53();
801
+
802
+ const roleCreation = await simIam.createRole(
803
+ new CreateRoleCommand({
804
+ RoleName: "UnprivilegedRole",
805
+ AssumeRolePolicyDocument: JSON.stringify({
806
+ Version: "2012-10-17",
807
+ Statement: {
808
+ Effect: "Allow",
809
+ Principal: { AWS: "arn:aws:iam::123456789012:root" },
810
+ Action: "sts:AssumeRole",
811
+ },
812
+ }),
813
+ }),
814
+ );
815
+
816
+ try {
817
+ await simRoute53.createHostedZone(
818
+ new CreateHostedZoneCommand({
819
+ Name: "denied.example.test",
820
+ CallerReference: "denied-ref",
821
+ }),
822
+ {
823
+ caller: { kind: "arn", arn: roleCreation.Role.Arn },
824
+ },
825
+ );
826
+ } catch (error) {
827
+ console.error("Hosted Zone creation denied", error);
828
+ }
829
+
830
+ await simRoute53.createHostedZone(
831
+ new CreateHostedZoneCommand({
832
+ Name: "allowed.example.test",
833
+ CallerReference: "allowed-ref",
834
+ }),
835
+ );
836
+ ```
837
+
838
+ A denied action throws an AWS-like access-denied error with a `403` status code and the attempted
839
+ action, resource, and caller for diagnostics, before the service mutates any state. Omitting the
840
+ caller defaults to the Account root. The Account root is allowed within its own Account, and a test
841
+ that never mentions IAM keeps working.
842
+
843
+ ## CloudFormation IAM resources
844
+
845
+ Sim CloudFormation can create IAM resources from `AWS::IAM::Role`, `AWS::IAM::User`,
846
+ `AWS::IAM::ManagedPolicy`, and `AWS::IAM::Policy`. An `AWS::IAM::Policy` puts its document onto each
847
+ Role named in `Roles` and each User named in `Users` as an inline policy. That is the shape CDK
848
+ grants such as `bucket.grantRead(fn)` and `bucket.grantRead(user)` synthesize as a "DefaultPolicy"
849
+ resource. Every entry names a Role or a User in the Stack's Account. An entry naming no simulated
850
+ principal fails the resource, and so does a policy naming no principal at all. A `Groups` property
851
+ still fails the resource.
852
+
853
+ A statement value of the wrong type fails the resource too. A `Ref` or an `Fn::GetAtt` naming a
854
+ Resource the template leaves out survives resolution and reaches IAM as an object where `Action` or
855
+ `Resource` takes a string. The put refuses it with `MalformedPolicyDocument`, naming the Role or
856
+ User, the policy and the statement holding it.
857
+
858
+ An `AWS::IAM::ManagedPolicy` also carries `Roles`, and attaches itself to each Role it names as it
859
+ is created (the attachment `AttachRolePolicy` records). A name no simulated Role in the Account
860
+ answers to fails the resource. Deleting the stack takes the policy back off the Roles still
861
+ carrying it, and then deletes the policy.
862
+
863
+ ```typescript sim-iam-cloudformation
864
+ /**
865
+ * Creating IAM resources through simulated CloudFormation.
866
+ */
867
+
868
+ import { GetRoleCommand } from "@aws-sdk/client-iam";
869
+ import { SimAws } from "@kensio/yulin";
870
+
871
+ const simAws = new SimAws();
872
+
873
+ const stack = await simAws.cloudFormation().deployTemplate({
874
+ stackName: "iam-stack",
875
+ template: {
876
+ Resources: {
877
+ ServiceRole: {
878
+ Type: "AWS::IAM::Role",
879
+ Properties: {
880
+ RoleName: "LambdaExecutionRole",
881
+ Path: "/service-role/",
882
+ AssumeRolePolicyDocument: {
883
+ Version: "2012-10-17",
884
+ Statement: {
885
+ Effect: "Allow",
886
+ Principal: { Service: "lambda.amazonaws.com" },
887
+ Action: "sts:AssumeRole",
888
+ },
889
+ },
890
+ Policies: [
891
+ {
892
+ PolicyName: "ReadReports",
893
+ PolicyDocument: {
894
+ Version: "2012-10-17",
895
+ Statement: {
896
+ Effect: "Allow",
897
+ Action: "s3:GetObject",
898
+ Resource: "arn:aws:s3:::reports-bucket/*",
899
+ },
900
+ },
901
+ },
902
+ ],
903
+ },
904
+ },
905
+ ReadOnlyPolicy: {
906
+ Type: "AWS::IAM::ManagedPolicy",
907
+ Properties: {
908
+ ManagedPolicyName: "ReadOnlyAccess",
909
+ Roles: [{ Ref: "ServiceRole" }],
910
+ PolicyDocument: {
911
+ Version: "2012-10-17",
912
+ Statement: {
913
+ Effect: "Allow",
914
+ Action: "s3:GetObject",
915
+ Resource: "*",
916
+ },
917
+ },
918
+ },
919
+ },
920
+ },
921
+ Outputs: {
922
+ RoleArn: {
923
+ Value: {
924
+ "Fn::GetAtt": ["ServiceRole", "Arn"],
925
+ },
926
+ },
927
+ PolicyArn: {
928
+ Value: {
929
+ Ref: "ReadOnlyPolicy",
930
+ },
931
+ },
932
+ },
933
+ },
934
+ });
935
+
936
+ await stack.waitForDeployComplete();
937
+
938
+ console.log(stack.output("RoleArn"));
939
+ console.log(stack.output("PolicyArn"));
940
+
941
+ const roleOut = await simAws.iam().getRole(
942
+ new GetRoleCommand({
943
+ RoleName: "LambdaExecutionRole",
944
+ }),
945
+ );
946
+
947
+ console.log(roleOut.Role.Arn);
948
+ ```
949
+
950
+ For `AWS::IAM::Role`, `Ref` returns the Role name and `Fn::GetAtt` supports `Arn` and `RoleId`. For
951
+ `AWS::IAM::ManagedPolicy`, `Ref` returns the Policy ARN. Both resource types default their name to
952
+ the logical ID when it is omitted, and inline `Policies` declared on a Role are stored as the
953
+ Role's inline policies. The `ReadOnlyAccess` policy above is attached to `LambdaExecutionRole`.
954
+ Authorization for that Role reads the policy document the template gave it.
955
+
956
+ ### Users
957
+
958
+ An `AWS::IAM::User` creates a User in the Stack's Account. `UserName` names it and falls back to the
959
+ Resource logical ID. `Path`, the inline `Policies` list and `ManagedPolicyArns` work as they do on a
960
+ Role, and both policy forms reach the authorization decision made for that User. `Ref` returns the
961
+ User name and `Fn::GetAtt` supports `Arn` and `UserId`.
962
+
963
+ `LoginProfile` gives the User a console password, the way real CloudFormation calls
964
+ `CreateLoginProfile`. The profile records the password, its creation date and
965
+ `PasswordResetRequired`. Real IAM never reads a password back, and neither does the simulator. A
966
+ test asserting on the password reads the User record from `SimIam.users`.
967
+
968
+ A separate `AWS::IAM::Policy` naming the User in `Users` puts its document onto the User as another
969
+ inline policy. That is what a CDK grant against a User synthesizes.
970
+
971
+ Group membership is a gap. A `Groups` entry fails the Resource. An empty list still deploys, and CDK
972
+ leaves `Groups` out of the template for a User that belongs to no group.
973
+
974
+ Deleting the Stack deletes the User. Its inline policies and managed policy attachments come off
975
+ first, as they do for a Role, and the User name is free for the same template to deploy again.
976
+
977
+ ```typescript sim-iam-cloudformation-user
978
+ /**
979
+ * Creating an IAM User through simulated CloudFormation.
980
+ */
981
+
982
+ import { SimAws } from "@kensio/yulin";
983
+
984
+ const simAws = new SimAws();
985
+
986
+ const stack = await simAws.cloudFormation().deployTemplate({
987
+ stackName: "iam-user-stack",
988
+ template: {
989
+ Resources: {
990
+ ReportsReadPolicy: {
991
+ Type: "AWS::IAM::ManagedPolicy",
992
+ Properties: {
993
+ ManagedPolicyName: "ReportsReadPolicy",
994
+ PolicyDocument: {
995
+ Version: "2012-10-17",
996
+ Statement: {
997
+ Effect: "Allow",
998
+ Action: "s3:GetObject",
999
+ Resource: "arn:aws:s3:::reports-bucket/*",
1000
+ },
1001
+ },
1002
+ },
1003
+ },
1004
+ ReportPublisher: {
1005
+ Type: "AWS::IAM::User",
1006
+ Properties: {
1007
+ UserName: "ReportPublisher",
1008
+ Path: "/application/",
1009
+ ManagedPolicyArns: [{ Ref: "ReportsReadPolicy" }],
1010
+ Policies: [
1011
+ {
1012
+ PolicyName: "WriteReports",
1013
+ PolicyDocument: {
1014
+ Version: "2012-10-17",
1015
+ Statement: {
1016
+ Effect: "Allow",
1017
+ Action: "s3:PutObject",
1018
+ Resource: "arn:aws:s3:::reports-bucket/*",
1019
+ },
1020
+ },
1021
+ },
1022
+ ],
1023
+ LoginProfile: {
1024
+ Password: "initial-console-password",
1025
+ PasswordResetRequired: true,
1026
+ },
1027
+ },
1028
+ },
1029
+ },
1030
+ Outputs: {
1031
+ UserArn: {
1032
+ Value: {
1033
+ "Fn::GetAtt": ["ReportPublisher", "Arn"],
1034
+ },
1035
+ },
1036
+ },
1037
+ },
1038
+ });
1039
+
1040
+ await stack.waitForDeployComplete();
1041
+
1042
+ const simIam = simAws.iam();
1043
+
1044
+ const decision = simIam.authorize({
1045
+ action: "s3:PutObject",
1046
+ resource: "arn:aws:s3:::reports-bucket/2026/summary.csv",
1047
+ caller: { kind: "arn", arn: stack.output("UserArn") },
1048
+ });
1049
+
1050
+ console.log(decision.isAllowed);
1051
+
1052
+ const user = simIam.users
1053
+ .values()
1054
+ .find((each) => each.userName === "ReportPublisher");
1055
+
1056
+ console.log(user?.loginProfile?.passwordResetRequired);
1057
+ ```
1058
+
1059
+ ## Accounts
1060
+
1061
+ IAM is account-scoped in AWS, and sim IAM matches that. Every Region scope of the same simulated
1062
+ Account shares one IAM state, while different Accounts are fully isolated from each other.
1063
+
1064
+ ```typescript sim-iam-account-scoping
1065
+ /**
1066
+ * Simulated IAM Account scoping.
1067
+ */
1068
+
1069
+ import { CreateUserCommand } from "@aws-sdk/client-iam";
1070
+ import { SimAws } from "@kensio/yulin";
1071
+
1072
+ const simAws = new SimAws();
1073
+
1074
+ const firstAccountIam = simAws.account("111111111111").iam();
1075
+ const secondAccountIam = simAws.account("222222222222").iam();
1076
+
1077
+ const firstUserOutput = await firstAccountIam.createUser(
1078
+ new CreateUserCommand({ UserName: "DeployUser" }),
1079
+ );
1080
+ const secondUserOutput = await secondAccountIam.createUser(
1081
+ new CreateUserCommand({ UserName: "DeployUser" }),
1082
+ );
1083
+
1084
+ console.log(firstUserOutput.User.Arn);
1085
+ console.log(secondUserOutput.User.Arn);
1086
+ ```
1087
+
1088
+ Principals from one Account get no implicit access to another Account's resources. Authorizing a
1089
+ caller from a different simulated Account results in an implicit deny unless both Accounts allow the
1090
+ request.
1091
+
1092
+ ## Cross-Account requests
1093
+
1094
+ A request whose caller belongs to a different Account from the resource is decided in both
1095
+ Accounts, as it is on AWS:
1096
+
1097
+ - The resource's Account must allow it through a resource policy, such as an S3 Bucket policy or a
1098
+ Lambda permission
1099
+ - The caller's Account must allow it through an identity policy on that principal
1100
+
1101
+ Either one on its own is denied. A resource policy naming another Account's principal delegates to
1102
+ that Account, and an Account cannot grant its own principals access to somebody else's resource. An
1103
+ explicit `Deny` on either side denies. Callers with no
1104
+ identity side, such as a service principal or an anonymous request, are unaffected, and are still
1105
+ allowed by a resource policy alone.
1106
+
1107
+ ```typescript sim-iam-cross-account
1108
+ /**
1109
+ * A cross-Account request needs an allow from both Accounts.
1110
+ */
1111
+
1112
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1113
+ import { SimAws } from "@kensio/yulin";
1114
+
1115
+ const simAws = new SimAws();
1116
+ const partnerRoleArn = "arn:aws:iam::222222222222:role/Reader";
1117
+
1118
+ // The Bucket's Account grants the partner Account's Role.
1119
+ const bucketPolicy = {
1120
+ document: {
1121
+ Version: "2012-10-17",
1122
+ Statement: {
1123
+ Effect: "Allow",
1124
+ Principal: { AWS: partnerRoleArn },
1125
+ Action: "s3:GetObject",
1126
+ Resource: "arn:aws:s3:::reports-bucket/*",
1127
+ },
1128
+ },
1129
+ } as const;
1130
+
1131
+ const request = {
1132
+ action: "s3:GetObject",
1133
+ resource: "arn:aws:s3:::reports-bucket/summary.csv",
1134
+ caller: { kind: "arn", arn: partnerRoleArn },
1135
+ resourcePolicies: [bucketPolicy],
1136
+ } as const;
1137
+
1138
+ const beforeIdentityPolicy = simAws
1139
+ .account("111111111111")
1140
+ .iam()
1141
+ .authorize(request);
1142
+
1143
+ // false: the partner Account has not allowed its Role to read anything.
1144
+ console.log(beforeIdentityPolicy.isAllowed);
1145
+ console.log(beforeIdentityPolicy.resourceAllowStatements.length); // 1
1146
+ console.log(beforeIdentityPolicy.identityAllowStatements.length); // 0
1147
+
1148
+ // The partner Account allows its own Role.
1149
+ const partnerIam = simAws.account("222222222222").iam();
1150
+
1151
+ await partnerIam.createRole(
1152
+ new CreateRoleCommand({
1153
+ RoleName: "Reader",
1154
+ AssumeRolePolicyDocument: JSON.stringify({
1155
+ Version: "2012-10-17",
1156
+ Statement: {
1157
+ Effect: "Allow",
1158
+ Principal: { AWS: "arn:aws:iam::222222222222:root" },
1159
+ Action: "sts:AssumeRole",
1160
+ },
1161
+ }),
1162
+ }),
1163
+ );
1164
+
1165
+ await partnerIam.putRolePolicy(
1166
+ new PutRolePolicyCommand({
1167
+ RoleName: "Reader",
1168
+ PolicyName: "ReadReports",
1169
+ PolicyDocument: JSON.stringify({
1170
+ Version: "2012-10-17",
1171
+ Statement: {
1172
+ Effect: "Allow",
1173
+ Action: "s3:GetObject",
1174
+ Resource: "arn:aws:s3:::reports-bucket/*",
1175
+ },
1176
+ }),
1177
+ }),
1178
+ );
1179
+
1180
+ // true: both Accounts now allow the request.
1181
+ console.log(simAws.account("111111111111").iam().authorize(request).isAllowed);
1182
+ ```
1183
+
1184
+ The caller's Account is resolved from the principal ARN. For its policies to count, that Account has
1185
+ to belong to the same `SimAws` instance. An Account the simulation was never told about grants
1186
+ nothing. On AWS, a principal ARN that was never given any permissions comes out the same way.
1187
+
1188
+ A standalone `SimIam` has no simulation around it, and so no other Account to ask. A principal whose
1189
+ ARN belongs to another Account is always denied, however permissive the resource policy. Anonymous
1190
+ and service-principal callers carry on as before, since they have no Account either way, and a
1191
+ resource policy still allows them. The Account ID is available as `simIam.accountId`. A test naming
1192
+ its own principals should build their ARNs from that.
1193
+
1194
+ ## Standalone SimIam
1195
+
1196
+ If you only need IAM alone, you can instantiate `SimIam` directly.
1197
+
1198
+ ```typescript sim-iam-standalone
1199
+ /**
1200
+ * Standalone simulated IAM instance.
1201
+ */
1202
+
1203
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
1204
+ import { SimIam } from "@kensio/yulin/iam";
1205
+
1206
+ const simIam = new SimIam();
1207
+
1208
+ const roleCreation = await simIam.createRole(
1209
+ new CreateRoleCommand({
1210
+ RoleName: "StandaloneRole",
1211
+ AssumeRolePolicyDocument: JSON.stringify({
1212
+ Version: "2012-10-17",
1213
+ Statement: {
1214
+ Effect: "Allow",
1215
+ Principal: { Service: "lambda.amazonaws.com" },
1216
+ Action: "sts:AssumeRole",
1217
+ },
1218
+ }),
1219
+ }),
1220
+ );
1221
+
1222
+ console.log(roleCreation.Role.Arn);
1223
+ ```
1224
+
1225
+ A standalone `SimIam` instance has its own isolated state, scoped to a generated Account ID, and
1226
+ stands apart from any wider `SimAws` environment. Other services instantiated standalone, such as
1227
+ `new SimRoute53()`, fall back to allow-all authorization. Connect services through a shared `SimAws`
1228
+ instance when a test should exercise real IAM enforcement.
1229
+
1230
+ ## Available functionality
1231
+
1232
+ Sim IAM currently supports:
1233
+
1234
+ - `CreateRoleCommand`, including trust-policy validation
1235
+ - `GetRoleCommand` and `ListRolesCommand`, with pagination
1236
+ - `PutRolePolicyCommand`, for inline Role policies
1237
+ - `CreatePolicyCommand`, `GetPolicyCommand` and `ListPoliciesCommand`, for managed Policies
1238
+ - `AttachRolePolicyCommand`
1239
+ - `CreateUserCommand`, `DeleteUserCommand`, `PutUserPolicyCommand` and `AttachUserPolicyCommand`
1240
+ - `CreateLoginProfileCommand`, for a User's console password
1241
+ - `CreateAccessKeyCommand`, registering access keys for credential authentication
1242
+ - Allow/deny authorization decisions with `authorize(...)`, evaluating identity policies,
1243
+ service-supplied resource policies, and policy conditions with explicit-deny precedence
1244
+ - IAM authorization at simulated service boundaries, such as Route53 actions
1245
+ - Resolving the caller of an HTTP request, from an `x-sim-aws-caller` header or a verified SigV4
1246
+ signature, defaulting to anonymous, and the resource it is made on behalf of from
1247
+ `x-sim-aws-source-arn` and `x-sim-aws-source-account`
1248
+ - Temporary Role sessions through simulated STS `AssumeRoleCommand`, evaluated against Role trust
1249
+ policies
1250
+ - The `AWS::IAM::Role`, `AWS::IAM::User`, `AWS::IAM::ManagedPolicy` and `AWS::IAM::Policy`
1251
+ CloudFormation resources
1252
+
1253
+ Unsupported IAM options may be ignored or may throw errors depending on whether the simulator needs
1254
+ them to model the requested behaviour.
1255
+
1256
+ ## Limitations
1257
+
1258
+ Sim IAM models the policy behaviour that multi-service tests most commonly need. Notable gaps:
1259
+
1260
+ - Groups are absent. An `AWS::IAM::User` naming one fails, and an `AWS::IAM::Policy` naming one
1261
+ fails too
1262
+ - Permissions boundaries and session policies are not evaluated. Service control policies are, and
1263
+ are attached through [simulated Organizations](https://yulinsim.dev/services/organizations/ "Simulated Organizations service control policies usage docs")
1264
+ - Managed Policies have a single version, and the policy version commands are absent
1265
+ - `DeleteUserPolicy` and `DetachUserPolicy` are absent, along with `DeleteAccessKey` and
1266
+ `DeleteLoginProfile`. `DeleteUserCommand` refuses a User that still holds a policy, and
1267
+ CloudFormation teardown clears a User's policies before deleting it
1268
+ - Only the condition operators listed above are supported. A statement using any other operator
1269
+ fails closed, matching no request. `decision.unevaluatedStatements` names those statements, and a
1270
+ test can assert that a decision was reached over policies read in full
1271
+ - A positive `ForAllValues:` condition fails to match a request carrying no value for the key, and
1272
+ fails to match an empty value set. AWS matches both, and the negated form here matches both
1273
+ - Signature age is deliberately not enforced. `X-Amz-Date` must be present, well formed, and agree
1274
+ with the credential scope date, but is never compared to a clock. A client stamping real time can
1275
+ therefore reach a simulation keeping a different one. Session expiry _is_ enforced, against
1276
+ simulated time
1277
+ - S3 `aws-chunked` streaming signatures and SigV4A are not verified. Presigned query-string
1278
+ authentication is verified, for the `AWS4-HMAC-SHA256` algorithm only
1279
+ - The resolved caller is evaluated by Lambda Function URLs with `AuthType: "AWS_IAM"` and by the S3
1280
+ REST endpoint, but not yet by the other services that serve HTTP. Website-endpoint S3 responses
1281
+ and CloudFront responses perform no authorization
1282
+ - The identity side of a cross-Account request comes from the caller's Account in the same `SimAws`
1283
+ instance. A caller whose Account is absent from the simulation is denied