@kensio/yulin 1.21.17 → 1.21.18

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.
@@ -0,0 +1,13 @@
1
+ import type { SimCfnResource } from "../../resource/sim-cfn-resource.js";
2
+ import type { SimCfnTemplateValueRecord } from "../../template/value/sim-cfn-template-value.js";
3
+ import { type SimLambdaFunctionArn } from "../../../lambda/function/sim-lambda-function-configuration.js";
4
+ /**
5
+ * The provider function a custom Resource's `ServiceToken` names, as an ARN.
6
+ *
7
+ * Nothing comes back where the Resource names no provider this Stack can put
8
+ * an ARN to. A `ServiceToken` built from a Parameter, or one pointing at a
9
+ * Resource type that is not a function, both leave the deployment with nothing
10
+ * to authorize against, and inventing an ARN for it would refuse a template
11
+ * over a permission real CloudFormation never asked for.
12
+ */
13
+ export declare function simCdkProviderInvokeArn(resolvedProperties: SimCfnTemplateValueRecord, resources: ReadonlyMap<string, SimCfnResource>): SimLambdaFunctionArn | undefined;
@@ -0,0 +1,63 @@
1
+ import { namesSimCfnUnansweredAttribute } from "../../resource/cfn/sim-cfn-unanswered-attribute.js";
2
+ import { parseSimLambdaFunctionArn } from "../../../lambda/function/sim-lambda-function-arn-parts.js";
3
+ import { simLambdaFunctionArn, } from "../../../lambda/function/sim-lambda-function-configuration.js";
4
+ const lambdaFunctionResourceType = "AWS::Lambda::Function";
5
+ /**
6
+ * The provider function a custom Resource's `ServiceToken` names, as an ARN.
7
+ *
8
+ * Nothing comes back where the Resource names no provider this Stack can put
9
+ * an ARN to. A `ServiceToken` built from a Parameter, or one pointing at a
10
+ * Resource type that is not a function, both leave the deployment with nothing
11
+ * to authorize against, and inventing an ARN for it would refuse a template
12
+ * over a permission real CloudFormation never asked for.
13
+ */
14
+ export function simCdkProviderInvokeArn(resolvedProperties, resources) {
15
+ const serviceToken = resolvedProperties["ServiceToken"];
16
+ if (typeof serviceToken !== "string" || serviceToken === "") {
17
+ return undefined;
18
+ }
19
+ return (writtenFunctionArn(serviceToken) ??
20
+ standInFunctionArn(serviceToken, resources));
21
+ }
22
+ /**
23
+ * The ARN a `ServiceToken` already carries.
24
+ *
25
+ * A provider deployed outside this Stack is named by its ARN outright, and a
26
+ * template written by hand can name one in the same Stack that way too.
27
+ */
28
+ function writtenFunctionArn(serviceToken) {
29
+ return parseSimLambdaFunctionArn(serviceToken) === undefined
30
+ ? undefined
31
+ : serviceToken;
32
+ }
33
+ /**
34
+ * The ARN behind a `ServiceToken` that resolved to an attribute stand-in.
35
+ *
36
+ * CDK writes `ServiceToken` as an `Fn::GetAtt` for the provider function's
37
+ * `Arn`, and this simulator leaves that provider uncreated: the function is
38
+ * declined on its Python runtime and then reported as scaffolding for the
39
+ * custom Resource the simulator carries out itself. An uncreated Resource
40
+ * answers `Fn::GetAtt` with {@link namesSimCfnUnansweredAttribute}'s stand-in
41
+ * rather than an ARN.
42
+ *
43
+ * The name survives that refusal. Sim Lambda works out the name real
44
+ * CloudFormation would have given the function before declining it, so the
45
+ * Resource's `Ref` holds a function name to build the ARN from. A provider
46
+ * that reached neither creation nor a name has only its logical ID behind
47
+ * `Ref`, and an ARN built from that would be denied by a policy naming the
48
+ * function real CloudFormation invokes.
49
+ */
50
+ function standInFunctionArn(serviceToken, resources) {
51
+ if (!namesSimCfnUnansweredAttribute(serviceToken, resources.keys())) {
52
+ return undefined;
53
+ }
54
+ const provider = resources.get(serviceToken.split(".", 1)[0] ?? "");
55
+ if (provider?.type !== lambdaFunctionResourceType ||
56
+ (!provider.deployed && provider.uncreatedPhysicalName === undefined)) {
57
+ return undefined;
58
+ }
59
+ const { refValue } = provider;
60
+ return typeof refValue === "string" && refValue !== ""
61
+ ? simLambdaFunctionArn(provider.accountRegionScope, refValue)
62
+ : undefined;
63
+ }
@@ -0,0 +1,36 @@
1
+ import type { SimAws } from "../../../aws/sim-aws.js";
2
+ import type { SimAwsCaller } from "../../../aws/caller/sim-aws-caller.js";
3
+ import type { SimCloudFormationParsedResourceType } from "../../resource/factory/sim-cfn-resource-factory.type.js";
4
+ import type { SimCfnResource } from "../../resource/sim-cfn-resource.js";
5
+ import type { SimCfnTemplateValueRecord } from "../../template/value/sim-cfn-template-value.js";
6
+ /**
7
+ * What authorizing one custom Resource's provider invoke needs.
8
+ */
9
+ export interface SimCdkProviderInvokeAuthZProperties {
10
+ readonly resourceType: SimCloudFormationParsedResourceType;
11
+ readonly resource: SimCfnResource;
12
+ readonly resolvedProperties: SimCfnTemplateValueRecord;
13
+ readonly resources: ReadonlyMap<string, SimCfnResource>;
14
+ readonly simAws: SimAws;
15
+ readonly caller?: SimAwsCaller | undefined;
16
+ }
17
+ /**
18
+ * Authorize the invoke a custom Resource costs the deployment.
19
+ *
20
+ * Real CloudFormation answers a `Custom::` Resource by invoking the function
21
+ * its `ServiceToken` names, as the Stack's execution role. That invoke is the
22
+ * first thing a deployment pays for, and a role without `lambda:InvokeFunction`
23
+ * on the provider fails the Stack there rather than at the work the provider
24
+ * would have done.
25
+ *
26
+ * This simulator carries several custom Resources out itself instead of
27
+ * running the provider, which leaves the invoke unpaid for unless it is
28
+ * authorized here. A deploy Role standing in for a real CloudFormation
29
+ * execution policy is meant to fail the same template the real one refuses.
30
+ *
31
+ * Only the caller's identity policies decide it. The provider function is
32
+ * uncreated in the usual case, so there is no simulated function holding a
33
+ * resource policy to consult, and CDK grants the invoke on the execution role
34
+ * anyway.
35
+ */
36
+ export declare function authorizeSimCdkProviderInvoke(properties: SimCdkProviderInvokeAuthZProperties): void;
@@ -0,0 +1,47 @@
1
+ import { simScopeIamAuthZ } from "../../../iam/authorize/sim-iam-region-auth-z.js";
2
+ import { SimIamAccessDenied } from "../../../iam/error/sim-iam.error.js";
3
+ import { simCdkProviderInvokeArn } from "./sim-cdk-provider-invoke-arn.js";
4
+ const customResourceProviderName = "Custom";
5
+ const invokeAction = "lambda:InvokeFunction";
6
+ /**
7
+ * Authorize the invoke a custom Resource costs the deployment.
8
+ *
9
+ * Real CloudFormation answers a `Custom::` Resource by invoking the function
10
+ * its `ServiceToken` names, as the Stack's execution role. That invoke is the
11
+ * first thing a deployment pays for, and a role without `lambda:InvokeFunction`
12
+ * on the provider fails the Stack there rather than at the work the provider
13
+ * would have done.
14
+ *
15
+ * This simulator carries several custom Resources out itself instead of
16
+ * running the provider, which leaves the invoke unpaid for unless it is
17
+ * authorized here. A deploy Role standing in for a real CloudFormation
18
+ * execution policy is meant to fail the same template the real one refuses.
19
+ *
20
+ * Only the caller's identity policies decide it. The provider function is
21
+ * uncreated in the usual case, so there is no simulated function holding a
22
+ * resource policy to consult, and CDK grants the invoke on the execution role
23
+ * anyway.
24
+ */
25
+ export function authorizeSimCdkProviderInvoke(properties) {
26
+ if (properties.resourceType.providerName !== customResourceProviderName) {
27
+ return;
28
+ }
29
+ const functionArn = simCdkProviderInvokeArn(properties.resolvedProperties, properties.resources);
30
+ if (functionArn === undefined) {
31
+ return;
32
+ }
33
+ const { accountId, regionName } = properties.resource.accountRegionScope;
34
+ const decision = simScopeIamAuthZ(properties.simAws.accountRegionScope(accountId, regionName)).authorize({
35
+ action: invokeAction,
36
+ resource: functionArn,
37
+ caller: properties.caller,
38
+ });
39
+ if (decision.isDenied) {
40
+ throw new SimIamAccessDenied({
41
+ principal: decision.caller.principal,
42
+ reason: decision.denialReason,
43
+ action: invokeAction,
44
+ resource: functionArn,
45
+ });
46
+ }
47
+ }
@@ -2,9 +2,11 @@ import { bucketNotificationsError } from "../error/sim-cdk-bucket-notification-e
2
2
  /**
3
3
  * The properties CDK puts on a Custom::S3BucketNotifications Resource.
4
4
  *
5
- * `ServiceToken` is read and ignored. It points at CDK's own Python provider
6
- * function, whose whole job is the PutBucketNotificationConfiguration call this
7
- * factory makes instead.
5
+ * `ServiceToken` is accepted and read no further here. It points at CDK's own
6
+ * Python provider function, whose whole job is the
7
+ * PutBucketNotificationConfiguration call this factory makes instead. The
8
+ * deployment reads it once more, to authorize the invoke real CloudFormation
9
+ * makes of that function.
8
10
  */
9
11
  const knownPropertyNames = new Set([
10
12
  "BucketName",
@@ -1,5 +1,6 @@
1
1
  import { bucketNotificationsError } from "./error/sim-cdk-bucket-notification-error.js";
2
2
  import { SimCdkBucketNotificationProperties } from "./property/sim-cdk-bucket-notification-properties.js";
3
+ import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
3
4
  /**
4
5
  * Removes the notification configuration a Custom::S3BucketNotifications
5
6
  * Resource put on a Bucket.
@@ -29,6 +30,6 @@ export class SimCdkBucketNotificationsRemover {
29
30
  Bucket: properties.bucketName,
30
31
  NotificationConfiguration: {},
31
32
  },
32
- });
33
+ }, simCfnResourceCallerOptions(context.caller));
33
34
  }
34
35
  }
@@ -16,10 +16,13 @@ import type { SimCfnResource, SimCloudFormationResourceCreateContext, SimCloudFo
16
16
  * bucket alongside the function's `AWS::Lambda::Permission` is the circular
17
17
  * dependency this Resource type exists to break.
18
18
  *
19
- * `ServiceToken` is read for the provider function it names, and otherwise
20
- * ignored. That function is declined on its runtime, as CDK's BucketDeployment
21
- * provider is, and recorded as inert rather than as a gap, since this factory
22
- * has already made the call it would have made.
19
+ * `ServiceToken` is read for the provider function it names. That function is
20
+ * declined on its runtime, as CDK's BucketDeployment provider is, and recorded
21
+ * as inert rather than as a gap, since this factory has already made the call
22
+ * it would have made. The deployment still pays for the invoke real
23
+ * CloudFormation makes, which
24
+ * {@link import("../../provider/sim-cdk-provider-invoke-auth-z.js").authorizeSimCdkProviderInvoke}
25
+ * authorizes for every custom Resource ahead of its factory.
23
26
  */
24
27
  export declare class SimCdkBucketNotificationsResourceFactory implements SimCfnServiceResourceFactory {
25
28
  /**
@@ -2,6 +2,7 @@ import { SimCdkBucketNotificationConfiguration } from "./configuration/sim-cdk-b
2
2
  import { bucketNotificationsError } from "./error/sim-cdk-bucket-notification-error.js";
3
3
  import { SimCdkBucketNotificationProperties } from "./property/sim-cdk-bucket-notification-properties.js";
4
4
  import { SimCdkBucketNotificationsRemover } from "./sim-cdk-bucket-notifications-remover.js";
5
+ import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
5
6
  /**
6
7
  * CloudFormation Resource factory for CDK Bucket event notifications.
7
8
  *
@@ -18,10 +19,13 @@ import { SimCdkBucketNotificationsRemover } from "./sim-cdk-bucket-notifications
18
19
  * bucket alongside the function's `AWS::Lambda::Permission` is the circular
19
20
  * dependency this Resource type exists to break.
20
21
  *
21
- * `ServiceToken` is read for the provider function it names, and otherwise
22
- * ignored. That function is declined on its runtime, as CDK's BucketDeployment
23
- * provider is, and recorded as inert rather than as a gap, since this factory
24
- * has already made the call it would have made.
22
+ * `ServiceToken` is read for the provider function it names. That function is
23
+ * declined on its runtime, as CDK's BucketDeployment provider is, and recorded
24
+ * as inert rather than as a gap, since this factory has already made the call
25
+ * it would have made. The deployment still pays for the invoke real
26
+ * CloudFormation makes, which
27
+ * {@link import("../../provider/sim-cdk-provider-invoke-auth-z.js").authorizeSimCdkProviderInvoke}
28
+ * authorizes for every custom Resource ahead of its factory.
25
29
  */
26
30
  export class SimCdkBucketNotificationsResourceFactory {
27
31
  /**
@@ -48,7 +52,7 @@ export class SimCdkBucketNotificationsResourceFactory {
48
52
  NotificationConfiguration: configuration,
49
53
  SkipDestinationValidation: properties.skipDestinationValidation,
50
54
  },
51
- });
55
+ }, simCfnResourceCallerOptions(context.caller));
52
56
  return undefined;
53
57
  }
54
58
  /**
@@ -5,9 +5,10 @@ import { crossRegionParameterError } from "./sim-cdk-cross-region-parameter-erro
5
5
  * The properties CDK puts on a Custom::CrossRegionStringParameterReader
6
6
  * Resource.
7
7
  *
8
- * `ServiceToken` is read and ignored. It points at CDK's own provider
9
- * function, whose whole job is the GetParameter call this factory makes
10
- * instead. `RefreshToken` is the logical ID of the function version the
8
+ * `ServiceToken` is accepted and read no further here. It points at CDK's own
9
+ * provider function, whose whole job is the GetParameter call this factory
10
+ * makes instead, and the deployment reads it once more to authorize the invoke
11
+ * real CloudFormation makes of that function. `RefreshToken` is the logical ID of the function version the
11
12
  * parameter was written from, and CDK puts it there so that publishing a new
12
13
  * version makes CloudFormation run the reader again. The reader here runs on
13
14
  * every deployment either way, so the token has nothing left to say.
@@ -2,6 +2,7 @@ import { SimSsmParameterNotFound } from "../../../../ssm/error/sim-ssm.error.js"
2
2
  import { crossRegionParameterError, crossRegionParameterResourceTypeName, } from "./sim-cdk-cross-region-parameter-error.js";
3
3
  import { SimCdkCrossRegionParameterProperties } from "./sim-cdk-cross-region-parameter-properties.js";
4
4
  import { SimCdkCrossRegionParameterReading } from "./sim-cdk-cross-region-parameter-reading.js";
5
+ import { simCfnResourceCallerOptions } from "../../../resource/caller/sim-cfn-resource-caller-options.js";
5
6
  /**
6
7
  * CloudFormation Resource factory for CDK's cross-Region parameter reader.
7
8
  *
@@ -64,7 +65,7 @@ export class SimCdkCrossRegionParameterReaderResourceFactory {
64
65
  const output = await context.simAws
65
66
  .accountRegionScope(resource.accountRegionScope.accountId, regionName)
66
67
  .ssm()
67
- .getParameter({ input: { Name: parameterName } });
68
+ .getParameter({ input: { Name: parameterName } }, simCfnResourceCallerOptions(context.caller));
68
69
  return output.Parameter?.Value;
69
70
  }
70
71
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { parseSimCloudFormationResourceType } from "../parser/sim-cfn-resource-parser.js";
2
2
  import { resolveSimCloudFormationServiceResourceFactory } from "../resolve/service/sim-cfn-service-resolver.js";
3
3
  import { assertDefined } from "../../../../util/type-guard/defined.js";
4
+ import { authorizeSimCdkProviderInvoke } from "../../cdk/provider/sim-cdk-provider-invoke-auth-z.js";
4
5
  /**
5
6
  * Creates the underlying simulated AWS service Resource for a CloudFormation
6
7
  * Resource.
@@ -35,9 +36,18 @@ export class SimCfnResourceCreator {
35
36
  const resourceType = parseSimCloudFormationResourceType(type);
36
37
  const factory = this.cfnResourceFactory ??
37
38
  resolveSimCloudFormationServiceResourceFactory(context.simAws, this.resource.accountRegionScope, resourceType);
39
+ const resolvedProperties = await this.resource.resolvedProperties(context);
40
+ authorizeSimCdkProviderInvoke({
41
+ resourceType,
42
+ resource: this.resource,
43
+ resolvedProperties,
44
+ resources: context.resources,
45
+ simAws: context.simAws,
46
+ caller: context.caller,
47
+ });
38
48
  const resolvedContext = {
39
49
  ...context,
40
- resolvedProperties: await this.resource.resolvedProperties(context),
50
+ resolvedProperties,
41
51
  };
42
52
  return await factory.create(resourceType.resourceTypeName, this.resource, resolvedContext);
43
53
  }
@@ -1,6 +1,7 @@
1
1
  import { parseSimCloudFormationResourceType } from "../parser/sim-cfn-resource-parser.js";
2
2
  import { resolveSimCloudFormationServiceResourceFactory } from "../resolve/service/sim-cfn-service-resolver.js";
3
3
  import { assertDefined } from "../../../../util/type-guard/defined.js";
4
+ import { authorizeSimCdkProviderInvoke } from "../../cdk/provider/sim-cdk-provider-invoke-auth-z.js";
4
5
  /**
5
6
  * Removes the underlying simulated AWS service Resource for a CloudFormation
6
7
  * Resource.
@@ -39,9 +40,18 @@ export class SimCfnResourceDeleter {
39
40
  const resourceType = parseSimCloudFormationResourceType(type);
40
41
  const factory = this.cfnResourceFactory ??
41
42
  resolveSimCloudFormationServiceResourceFactory(context.simAws, this.resource.accountRegionScope, resourceType);
43
+ const resolvedProperties = await this.resource.resolvedProperties(context);
44
+ authorizeSimCdkProviderInvoke({
45
+ resourceType,
46
+ resource: this.resource,
47
+ resolvedProperties,
48
+ resources: context.resources,
49
+ simAws: context.simAws,
50
+ caller: context.caller,
51
+ });
42
52
  await factory.delete(resourceType.resourceTypeName, this.resource, {
43
53
  ...context,
44
- resolvedProperties: await this.resource.resolvedProperties(context),
54
+ resolvedProperties,
45
55
  });
46
56
  }
47
57
  }
@@ -2549,6 +2549,94 @@ working simulation with a hand-written one.
2549
2549
  The provider is found through the `ServiceToken` its custom resource names it by, not by the logical
2550
2550
  ID CDK generated for it. That ID is a hash of the construct path, and no kind of thing to match on.
2551
2551
 
2552
+ ### What a custom resource costs the deploy Role
2553
+
2554
+ Real CloudFormation answers a `Custom::` resource by invoking the provider function its `ServiceToken`
2555
+ names, as the stack's execution role. Yulin carries several of these out itself instead of running the
2556
+ provider, and it authorizes that invoke anyway. A deploy Role holding no `lambda:InvokeFunction` on the
2557
+ provider fails the stack where the real deployment fails.
2558
+
2559
+ The `ServiceToken` decides the ARN. A token written as an ARN is taken as it stands. CDK's `Fn::GetAtt`
2560
+ for the provider's `Arn` is resolved back to the function the stack declares, since the provider is
2561
+ left uncreated and has no ARN of its own to give.
2562
+
2563
+ The work the resource does is authorized too, as the deployment's principal.
2564
+ `Custom::S3BucketNotifications` costs `s3:PutBucketNotification` and
2565
+ `Custom::CrossRegionStringParameterReader` costs `ssm:GetParameter`. `Custom::CDKBucketDeployment`
2566
+ writes its Objects into the simulated Bucket without sending `PutObject`, so nothing there reaches IAM.
2567
+
2568
+ ```typescript sim-cloudformation-custom-resource-invoke
2569
+ /**
2570
+ * A deploy Role refused the provider invoke its custom resource needs.
2571
+ */
2572
+
2573
+ import { SimAws } from "@kensio/yulin";
2574
+
2575
+ const simAws = new SimAws();
2576
+
2577
+ const { Role } = await simAws.iam().createRole({
2578
+ input: {
2579
+ RoleName: "DeployRole",
2580
+ AssumeRolePolicyDocument: JSON.stringify({
2581
+ Version: "2012-10-17",
2582
+ Statement: [
2583
+ {
2584
+ Effect: "Allow",
2585
+ Principal: { Service: "cloudformation.amazonaws.com" },
2586
+ Action: "sts:AssumeRole",
2587
+ },
2588
+ ],
2589
+ }),
2590
+ },
2591
+ });
2592
+
2593
+ // Everything the template needs apart from the invoke of the provider.
2594
+ await simAws.iam().putRolePolicy({
2595
+ input: {
2596
+ RoleName: "DeployRole",
2597
+ PolicyName: "DeployPolicy",
2598
+ PolicyDocument: JSON.stringify({
2599
+ Version: "2012-10-17",
2600
+ Statement: [
2601
+ {
2602
+ Effect: "Allow",
2603
+ Action: ["cloudformation:*", "s3:*"],
2604
+ Resource: "*",
2605
+ },
2606
+ ],
2607
+ }),
2608
+ },
2609
+ });
2610
+
2611
+ try {
2612
+ await simAws.cloudFormation().deployTemplate({
2613
+ stackName: "uploads-stack",
2614
+ caller: { kind: "arn", arn: Role.Arn },
2615
+ template: {
2616
+ Resources: {
2617
+ Bucket: {
2618
+ Type: "AWS::S3::Bucket",
2619
+ Properties: { BucketName: "uploads" },
2620
+ },
2621
+ BucketNotifications: {
2622
+ Type: "Custom::S3BucketNotifications",
2623
+ Properties: {
2624
+ ServiceToken: "arn:aws:lambda:us-east-1:888888888888:function:cdk",
2625
+ BucketName: { Ref: "Bucket" },
2626
+ NotificationConfiguration: {},
2627
+ Managed: true,
2628
+ },
2629
+ },
2630
+ },
2631
+ },
2632
+ });
2633
+ } catch (error) {
2634
+ // is not authorized to perform: lambda:InvokeFunction on resource:
2635
+ // arn:aws:lambda:us-east-1:888888888888:function:cdk
2636
+ console.log((error as Error).message);
2637
+ }
2638
+ ```
2639
+
2552
2640
  ## S3 Bucket notifications
2553
2641
 
2554
2642
  The `NotificationConfiguration` property of `AWS::S3::Bucket` deploys through the ordinary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.17",
3
+ "version": "1.21.18",
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/",