@kensio/yulin 1.21.5 → 1.21.6

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 (63) hide show
  1. package/dist/service/cloudformation/resource/cfn/sim-cfn-default-resource-value-adapter.d.ts +8 -1
  2. package/dist/service/cloudformation/resource/cfn/sim-cfn-default-resource-value-adapter.js +9 -2
  3. package/dist/service/cloudformation/resource/cfn/sim-cfn-resource-value-adapter.d.ts +6 -1
  4. package/dist/service/cloudformation/resource/cfn/sim-cfn-resource-value-adapter.js +2 -1
  5. package/dist/service/cloudformation/resource/create/sim-cfn-resource-create-operation.d.ts +4 -0
  6. package/dist/service/cloudformation/resource/create/sim-cfn-resource-create-operation.js +6 -0
  7. package/dist/service/cloudformation/resource/sim-cfn-resource-record.d.ts +19 -0
  8. package/dist/service/cloudformation/resource/sim-cfn-resource-record.js +26 -0
  9. package/dist/service/cloudformation/resource/sim-cfn-resource.js +1 -0
  10. package/dist/service/cloudformation/resource/unsupported/sim-cfn-named-skip.d.ts +26 -0
  11. package/dist/service/cloudformation/resource/unsupported/sim-cfn-named-skip.js +31 -0
  12. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-creator.d.ts +5 -2
  13. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-creator.js +10 -2
  14. package/dist/service/cloudfront/cfn/origin-access-control/sim-cfn-cf-oac-creator.d.ts +5 -2
  15. package/dist/service/cloudfront/cfn/origin-access-control/sim-cfn-cf-oac-creator.js +10 -2
  16. package/dist/service/cloudfront/cfn/origin-request-policy/sim-cfn-cf-orp-creator.d.ts +5 -2
  17. package/dist/service/cloudfront/cfn/origin-request-policy/sim-cfn-cf-orp-creator.js +10 -2
  18. package/dist/service/cloudfront/cfn/policy/sim-cfn-cf-policy-creators.d.ts +33 -0
  19. package/dist/service/cloudfront/cfn/policy/sim-cfn-cf-policy-creators.js +39 -0
  20. package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-creator.d.ts +5 -2
  21. package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-creator.js +10 -2
  22. package/dist/service/cloudfront/cfn/sim-cfn-cf-authorizer.d.ts +40 -0
  23. package/dist/service/cloudfront/cfn/sim-cfn-cf-authorizer.js +50 -0
  24. package/dist/service/cloudfront/cfn/sim-cfn-cloudfront-resource-factory.d.ts +1 -4
  25. package/dist/service/cloudfront/cfn/sim-cfn-cloudfront-resource-factory.js +14 -46
  26. package/dist/service/cloudfront/sim-cloudfront-commands.d.ts +6 -0
  27. package/dist/service/cloudfront/sim-cloudfront-commands.js +7 -0
  28. package/dist/service/cloudfront/sim-cloudfront.d.ts +6 -0
  29. package/dist/service/cloudfront/sim-cloudfront.js +7 -0
  30. package/dist/service/dynamodb/document/sim-dynamodb-document-command-paths.d.ts +8 -0
  31. package/dist/service/dynamodb/document/sim-dynamodb-document-command-paths.js +62 -0
  32. package/dist/service/dynamodb/document/sim-dynamodb-document-routes.d.ts +1 -1
  33. package/dist/service/dynamodb/document/sim-dynamodb-document-routes.js +3 -1
  34. package/dist/service/iam/authorize/sim-iam-action-authorizer.d.ts +8 -3
  35. package/dist/service/iam/authorize/sim-iam-action-authorizer.js +13 -4
  36. package/dist/service/iam/cfn/role/sim-cfn-iam-role-creator.js +1 -0
  37. package/dist/service/iam/cfn/role/sim-cfn-iam-role-properties-parser.d.ts +1 -0
  38. package/dist/service/iam/cfn/role/sim-cfn-iam-role-properties-parser.js +21 -14
  39. package/dist/service/iam/cfn/{sim-cfn-iam-optional-string.d.ts → sim-cfn-iam-property.d.ts} +10 -2
  40. package/dist/service/iam/cfn/sim-cfn-iam-property.js +31 -0
  41. package/dist/service/iam/cfn/user/sim-cfn-iam-user-properties-parser.js +1 -1
  42. package/dist/service/iam/command/role/create-role/create-role-record-factory.js +3 -0
  43. package/dist/service/iam/command/role/create-role/create-role.command.d.ts +10 -0
  44. package/dist/service/iam/command/role/get-role/get-role.command.d.ts +2 -0
  45. package/dist/service/iam/command/role/get-role/get-role.handler.js +2 -0
  46. package/dist/service/iam/command/role/sim-iam-role-command-handlers.js +15 -1
  47. package/dist/service/iam/role/sim-iam-role-boundary.d.ts +24 -0
  48. package/dist/service/iam/role/sim-iam-role-boundary.js +24 -0
  49. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +1 -0
  50. package/dist/service/iam/role/sim-iam-role.d.ts +10 -0
  51. package/dist/service/iam/serve/sim-iam-query-entity.d.ts +6 -1
  52. package/dist/service/iam/serve/sim-iam-query-entity.js +10 -4
  53. package/dist/service/iam/serve/sim-iam-query-role-operations.js +15 -3
  54. package/dist/service/lambda/cfn/function/sim-cfn-lambda-runtime-skip.d.ts +6 -0
  55. package/dist/service/lambda/cfn/function/sim-cfn-lambda-runtime-skip.js +9 -2
  56. package/dist/service/route53/command/create-hosted-zone/sim-route53-zone-id.js +23 -1
  57. package/docs/services/cloudformation/README.md +88 -23
  58. package/docs/services/cloudfront/README.md +109 -0
  59. package/docs/services/dynamodb/README.md +128 -8
  60. package/docs/services/iam/README.md +125 -2
  61. package/docs/services/route53/README.md +11 -0
  62. package/package.json +1 -1
  63. package/dist/service/iam/cfn/sim-cfn-iam-optional-string.js +0 -17
@@ -2443,6 +2443,123 @@ console.log(output.Responses[1]); // {}
2443
2443
  That is the difference from `BatchGetItem`, which leaves a missing item out of its answer
2444
2444
  altogether. Here the answers stay lined up with the Gets that asked for them.
2445
2445
 
2446
+ ### Transactions through the document client
2447
+
2448
+ `TransactWriteCommand` and `TransactGetCommand` from `@aws-sdk/lib-dynamodb` reach the same two
2449
+ operations. Every action's `Item`, `Key` and `ExpressionAttributeValues` are converted on the way in,
2450
+ and the `Item` of each `Responses` entry on the way out.
2451
+
2452
+ ```typescript sim-dynamodb-document-transactions
2453
+ /**
2454
+ * Writing an order and the claim on its code together, in plain JavaScript.
2455
+ */
2456
+
2457
+ import { CreateTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
2458
+ import {
2459
+ DynamoDBDocumentClient,
2460
+ TransactGetCommand,
2461
+ TransactWriteCommand,
2462
+ } from "@aws-sdk/lib-dynamodb";
2463
+
2464
+ import { SimSdk } from "@kensio/yulin/sdk";
2465
+
2466
+ using simSdk = new SimSdk();
2467
+
2468
+ const documents = DynamoDBDocumentClient.from(
2469
+ new DynamoDBClient({ region: "eu-west-2" }),
2470
+ );
2471
+ simSdk.intercept(documents);
2472
+
2473
+ for (const [tableName, keyName] of [
2474
+ ["OrdersTable", "orderId"],
2475
+ ["ClaimsTable", "code"],
2476
+ ] as const) {
2477
+ await documents.send(
2478
+ new CreateTableCommand({
2479
+ TableName: tableName,
2480
+ KeySchema: [{ AttributeName: keyName, KeyType: "HASH" }],
2481
+ AttributeDefinitions: [{ AttributeName: keyName, AttributeType: "S" }],
2482
+ BillingMode: "PAY_PER_REQUEST",
2483
+ }),
2484
+ );
2485
+ }
2486
+ await simSdk.simAws.backgroundTasksComplete();
2487
+
2488
+ const claim = {
2489
+ TableName: "ClaimsTable",
2490
+ ConditionExpression: "attribute_not_exists(code)",
2491
+ };
2492
+
2493
+ // The order and the claim on its code go in together.
2494
+ await documents.send(
2495
+ new TransactWriteCommand({
2496
+ TransactItems: [
2497
+ {
2498
+ Put: {
2499
+ TableName: "OrdersTable",
2500
+ Item: { orderId: "order-1", total: 42, lines: [{ sku: "widget" }] },
2501
+ },
2502
+ },
2503
+ { Put: { ...claim, Item: { code: "ABC123", orderId: "order-1" } } },
2504
+ ],
2505
+ }),
2506
+ );
2507
+
2508
+ const read = await documents.send(
2509
+ new TransactGetCommand({
2510
+ TransactItems: [
2511
+ { Get: { TableName: "OrdersTable", Key: { orderId: "order-1" } } },
2512
+ { Get: { TableName: "ClaimsTable", Key: { code: "ABC123" } } },
2513
+ ],
2514
+ }),
2515
+ );
2516
+
2517
+ const lines = read.Responses?.[0]?.Item?.["lines"] as { sku: string }[];
2518
+ console.log(lines[0]?.sku); // widget
2519
+ console.log(read.Responses?.[1]?.Item?.["orderId"]); // order-1
2520
+
2521
+ // A second order wanting the same code loses the claim, and loses the order
2522
+ // with it.
2523
+ try {
2524
+ await documents.send(
2525
+ new TransactWriteCommand({
2526
+ TransactItems: [
2527
+ {
2528
+ Put: {
2529
+ TableName: "OrdersTable",
2530
+ Item: { orderId: "order-2", total: 7 },
2531
+ },
2532
+ },
2533
+ { Put: { ...claim, Item: { code: "ABC123", orderId: "order-2" } } },
2534
+ ],
2535
+ }),
2536
+ );
2537
+ } catch (error) {
2538
+ const cancelled = error as {
2539
+ name: string;
2540
+ CancellationReasons?: { Code: string }[];
2541
+ };
2542
+
2543
+ console.log(cancelled.name); // "TransactionCanceledException"
2544
+ console.log(cancelled.CancellationReasons?.map((reason) => reason.Code));
2545
+ // ["None", "ConditionalCheckFailed"]
2546
+ }
2547
+
2548
+ const second = await documents.send(
2549
+ new TransactGetCommand({
2550
+ TransactItems: [
2551
+ { Get: { TableName: "OrdersTable", Key: { orderId: "order-2" } } },
2552
+ ],
2553
+ }),
2554
+ );
2555
+
2556
+ console.log(second.Responses?.[0]); // {}
2557
+ ```
2558
+
2559
+ `CancellationReasons` arrive as the low-level Command reports them. There is one per action, in
2560
+ `TransactItems` order. A reason's `Item` holds AttributeValues, because a cancelled transaction is
2561
+ thrown, and the real document client converts nothing on the way out of a transactional write.
2562
+
2446
2563
  ## Expiring items with time to live
2447
2564
 
2448
2565
  `UpdateTimeToLive` names the attribute a table expires items by, and `DescribeTimeToLive` reports
@@ -2930,8 +3047,11 @@ console.log(tags.has("priority")); // true
2930
3047
  ```
2931
3048
 
2932
3049
  `PutCommand`, `GetCommand`, `DeleteCommand`, `UpdateCommand`, `QueryCommand`, `ScanCommand`,
2933
- `BatchWriteCommand` and `BatchGetCommand` are converted. A document Command with no route here, such
2934
- as `TransactWriteCommand`, is refused by name before anything tries to convert its values.
3050
+ `BatchWriteCommand`, `BatchGetCommand`, `TransactWriteCommand` and `TransactGetCommand` are
3051
+ converted. The two transactional ones have
3052
+ [a section of their own](#transactions-through-the-document-client). A document Command with no route
3053
+ here, such as the PartiQL `ExecuteStatementCommand`, is refused by name before anything tries to
3054
+ convert its values.
2935
3055
 
2936
3056
  Intercept the document client itself. `DynamoDBDocumentClient.from(client)` builds a separate object
2937
3057
  outside the `DynamoDBClient` class, so intercepting the base client leaves Commands sent through the
@@ -3621,15 +3741,15 @@ Arn`, `Fn::GetAtt … StreamArn` and `Fn::GetAtt … TableId` answering. A CDK `
3621
3741
  - SDK interception, and an intercepted `DynamoDBClient` or `DynamoDBStreamsClient` reaches the
3622
3742
  simulation.
3623
3743
  - The `@aws-sdk/lib-dynamodb` document client, with `PutCommand`, `GetCommand`, `DeleteCommand`,
3624
- `UpdateCommand`, `QueryCommand`, `ScanCommand`, `BatchWriteCommand` and `BatchGetCommand`
3625
- converting native JavaScript values on the way in and out, and `paginateQuery` and `paginateScan`
3626
- paging through a simulated table.
3744
+ `UpdateCommand`, `QueryCommand`, `ScanCommand`, `BatchWriteCommand`, `BatchGetCommand`,
3745
+ `TransactWriteCommand` and `TransactGetCommand` converting native JavaScript values on the way in
3746
+ and out, and `paginateQuery` and `paginateScan` paging through a simulated table.
3627
3747
 
3628
3748
  ## Limitations
3629
3749
 
3630
- - The document client's transaction and PartiQL Commands go unconverted. PartiQL is an operation
3631
- this simulation lacks yet. The transactions are simulated as operations, so only the document form
3632
- of them is missing. Both are refused by name, never half converted.
3750
+ - The document client's PartiQL Commands go unconverted, because PartiQL is an operation this
3751
+ simulation lacks yet. `ExecuteStatementCommand`, `BatchExecuteStatementCommand` and
3752
+ `ExecuteTransactionCommand` are refused by name, never half converted.
3633
3753
  - A document client's translate config goes unread. The marshalling options it was built with do not
3634
3754
  apply. See [the SDK docs](https://yulinsim.dev/sdk/#limitations).
3635
3755
  - `Expected`, `ConditionalOperator`, `AttributeUpdates`, `KeyConditions`, `QueryFilter` and
@@ -260,6 +260,11 @@ from the resolved caller and `aws:RequestedRegion` from the Region the request w
260
260
  service supplies under either name is overwritten by the derived one. Context-key names are matched
261
261
  case-insensitively, while string values remain case-sensitive.
262
262
 
263
+ IAM's own requests carry two service keys. A Role handed to a service carries
264
+ [`iam:PassedToService`](#passing-a-role-to-a-service), and a `CreateRole` request carries the
265
+ boundary it declares as
266
+ [`iam:PermissionsBoundary`](#creating-a-role-under-a-permissions-boundary).
267
+
263
268
  Every simulated service supplies its own Region. A policy conditioned on `aws:RequestedRegion`
264
269
  therefore sees the Region of the service that handled the request, whichever Region the caller was
265
270
  in, and a CloudFormation deployment carries the Region of the Stack it is deploying. IAM, CloudFront
@@ -1045,6 +1050,115 @@ await simAws.backgroundTasksComplete();
1045
1050
  A request naming no caller is decided as the Account root, which may pass any Role. A test that never
1046
1051
  mentions IAM keeps working.
1047
1052
 
1053
+ ## Creating a Role under a permissions boundary
1054
+
1055
+ An account that requires every Role to carry a permissions boundary allows `iam:CreateRole` only
1056
+ under `StringEquals` on `iam:PermissionsBoundary`. Its CloudFormation execution policy is written
1057
+ that way, and the CDK `PermissionsBoundary.fromName(...)` aspect puts the boundary ARN on every Role
1058
+ the app synthesizes. A `CreateRoleCommand` carrying `PermissionsBoundary` supplies the key, and
1059
+ `AWS::IAM::Role` supplies it from the template property of the same name.
1060
+
1061
+ A request that leaves the property out leaves the key unset. A positive operator over an unset key
1062
+ matches nothing, and the guard refuses the Role. A request naming some other policy is refused the
1063
+ same way. A test can assert both refusals, and that is what makes the policy worth standing up as a
1064
+ deploy Role.
1065
+
1066
+ `CreateRole` and `GetRole` then describe the boundary the Role carries, in the attachment shape IAM
1067
+ answers with (`PermissionsBoundaryType` and `PermissionsBoundaryArn`). `ListRoles` leaves the field
1068
+ out, as IAM does, and points a caller at `GetRole`. The boundary is not yet evaluated as a policy
1069
+ source of its own, and does not narrow what the Role may then do
1070
+ (see [Limitations](#limitations)).
1071
+
1072
+ ```typescript sim-iam-permissions-boundary
1073
+ /**
1074
+ * Deploying into an account that requires a permissions boundary.
1075
+ */
1076
+
1077
+ import { GetRoleCommand } from "@aws-sdk/client-iam";
1078
+ import { SimAws } from "@kensio/yulin";
1079
+
1080
+ const simAws = new SimAws({ defaultAccountId: "123456789012" });
1081
+ const boundaryArn = "arn:aws:iam::123456789012:policy/DeveloperBoundary";
1082
+
1083
+ const deployer = await simAws.iam().makeDeployRole({
1084
+ roleName: "cfn-exec",
1085
+ policyDocument: {
1086
+ Version: "2012-10-17",
1087
+ Statement: [
1088
+ {
1089
+ Effect: "Allow",
1090
+ Action: "iam:CreateRole",
1091
+ Resource: "*",
1092
+ Condition: {
1093
+ StringEquals: { "iam:PermissionsBoundary": boundaryArn },
1094
+ },
1095
+ },
1096
+ {
1097
+ Effect: "Allow",
1098
+ Action: ["cloudformation:*", "iam:PutRolePolicy", "iam:PassRole"],
1099
+ Resource: "*",
1100
+ },
1101
+ ],
1102
+ },
1103
+ });
1104
+
1105
+ const jobRoleTrust = {
1106
+ Version: "2012-10-17",
1107
+ Statement: {
1108
+ Effect: "Allow",
1109
+ Action: "sts:AssumeRole",
1110
+ Principal: { Service: "lambda.amazonaws.com" },
1111
+ },
1112
+ };
1113
+
1114
+ const stack = await simAws.cloudFormation().deployTemplate({
1115
+ stackName: "guarded-stack",
1116
+ template: {
1117
+ Resources: {
1118
+ JobRole: {
1119
+ Type: "AWS::IAM::Role",
1120
+ Properties: {
1121
+ RoleName: "JobRole",
1122
+ PermissionsBoundary: boundaryArn,
1123
+ AssumeRolePolicyDocument: jobRoleTrust,
1124
+ },
1125
+ },
1126
+ },
1127
+ },
1128
+ caller: deployer,
1129
+ });
1130
+
1131
+ // CREATE_COMPLETE
1132
+ console.log(stack.getResource("JobRole")?.status);
1133
+
1134
+ const roleRead = await simAws
1135
+ .iam()
1136
+ .getRole(new GetRoleCommand({ RoleName: "JobRole" }));
1137
+
1138
+ // arn:aws:iam::123456789012:policy/DeveloperBoundary
1139
+ console.log(roleRead.Role.PermissionsBoundary?.PermissionsBoundaryArn);
1140
+
1141
+ try {
1142
+ await simAws.cloudFormation().deployTemplate({
1143
+ stackName: "unguarded-stack",
1144
+ template: {
1145
+ Resources: {
1146
+ BareRole: {
1147
+ Type: "AWS::IAM::Role",
1148
+ Properties: {
1149
+ RoleName: "BareRole",
1150
+ AssumeRolePolicyDocument: jobRoleTrust,
1151
+ },
1152
+ },
1153
+ },
1154
+ },
1155
+ caller: deployer,
1156
+ });
1157
+ } catch (error) {
1158
+ console.error("A Role declaring no boundary was refused", error);
1159
+ }
1160
+ ```
1161
+
1048
1162
  ## A Role for a CloudFormation deployment
1049
1163
 
1050
1164
  `makeDeployRole(...)` creates a Role from a policy document and hands back a caller. Pass it as
@@ -1241,6 +1355,11 @@ the logical ID when it is omitted, and inline `Policies` declared on a Role are
1241
1355
  Role's inline policies. The `ReadOnlyAccess` policy above is attached to `LambdaExecutionRole`.
1242
1356
  Authorization for that Role reads the policy document the template gave it.
1243
1357
 
1358
+ A Role's `PermissionsBoundary` reaches the authorization decision the deployment is made under, and
1359
+ is recorded on the Role it names.
1360
+ [Creating a Role under a permissions boundary](#creating-a-role-under-a-permissions-boundary) covers
1361
+ the guard it exists for.
1362
+
1244
1363
  ### Users
1245
1364
 
1246
1365
  An `AWS::IAM::User` creates a User in the Stack's Account. `UserName` names it and falls back to the
@@ -1534,6 +1653,8 @@ Sim IAM currently supports:
1534
1653
  - IAM authorization at simulated service boundaries, such as Route53 actions
1535
1654
  - `iam:PassRole` authorization of a Role handed to simulated Lambda, Scheduler, EventBridge, Step
1536
1655
  Functions, ECS or Firehose, with `iam:PassedToService` supplied
1656
+ - `iam:CreateRole` authorization against `iam:PermissionsBoundary`, from the `PermissionsBoundary`
1657
+ a request or an `AWS::IAM::Role` declares
1537
1658
  - Resolving the caller of an HTTP request, from an `x-sim-aws-caller` header or a verified SigV4
1538
1659
  signature, defaulting to anonymous, and the resource it is made on behalf of from
1539
1660
  `x-sim-aws-source-arn` and `x-sim-aws-source-account`
@@ -1551,8 +1672,10 @@ Sim IAM models the policy behaviour that multi-service tests most commonly need.
1551
1672
 
1552
1673
  - Groups are absent. An `AWS::IAM::User` naming one fails, and an `AWS::IAM::Policy` naming one
1553
1674
  fails too
1554
- - Permissions boundaries and session policies are not evaluated. Service control policies are, and
1555
- are attached through [simulated Organizations](https://yulinsim.dev/services/organizations/ "Simulated Organizations service control policies usage docs")
1675
+ - Permissions boundaries and session policies are not evaluated. A boundary a `CreateRole` request
1676
+ declares is authorized against `iam:PermissionsBoundary` and described by `GetRole`, and stops
1677
+ there. Service control policies are evaluated, and are attached through
1678
+ [simulated Organizations](https://yulinsim.dev/services/organizations/ "Simulated Organizations service control policies usage docs")
1556
1679
  - Managed Policies have a single version, and the policy version commands are absent
1557
1680
  - A policy document is measured against IAM's character limit one document at a time. The
1558
1681
  20,480-character cap on the sum of a User's inline policies is absent, and so is the cap on how
@@ -139,6 +139,17 @@ Register the zone yourself when a test depends on its name, such as one listing
139
139
  when its name is a suffix of no record the stack holds. A registered zone keeps the name it was
140
140
  given, and its records are stored under the name you chose.
141
141
 
142
+ ### A lookup that never resolved
143
+
144
+ `HostedZone.fromLookup` needs credentials for the account holding the zone, or a `cdk.context.json`
145
+ holding a previous lookup's answer. With neither, CDK synthesizes the literal `DUMMY` as the Hosted
146
+ Zone ID and writes it into every `AWS::Route53::RecordSet` in the template. Deploying that template
147
+ gives `InvalidInput`, naming `DUMMY` as the stand-in and pointing back at the synth.
148
+
149
+ Run `cdk synth` with credentials for that account, or commit the `cdk.context.json` a resolved
150
+ lookup writes. Either one puts the real Hosted Zone ID in the template, and the zone is then
151
+ registered on demand like any other looked-up zone.
152
+
142
153
  ## Creating records
143
154
 
144
155
  Use `ChangeResourceRecordSetsCommand` to add records to a Hosted Zone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.5",
3
+ "version": "1.21.6",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",
@@ -1,17 +0,0 @@
1
- /**
2
- * Read a string property an IAM Resource may leave out.
3
- *
4
- * A Role and a User read most of their own properties this way, and both
5
- * refuse a non-string the same way, naming the Resource type the template
6
- * declared.
7
- */
8
- export function simCfnIamOptionalString(properties) {
9
- const { resourceType, resource, value, label } = properties;
10
- if (value === undefined) {
11
- return undefined;
12
- }
13
- if (typeof value !== "string") {
14
- throw new TypeError(`Invalid ${resourceType} ${resource.logicalId}: ${label} must be a string`);
15
- }
16
- return value;
17
- }