@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.
- package/README.md +13 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- 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
|