From 62f08f4c80a0f29063b6d6188c0b39332eb786f5 Mon Sep 17 00:00:00 2001 From: Chelsea Urquhart Date: Sun, 21 Apr 2024 07:07:07 -0700 Subject: [PATCH] fix(stepfunctions): distributed maps under branches distributed maps under branch states (i.e., Parallel) do not apply the necessary permissions to run the state. this moves the bind functionality into state and calls it on both state and all child states. rather than relying on the single purpose that it is now (add distributed map perms) and fast returning all the way out, this instead just checks if the policy it is trying to add is in place before proceeding and uses that condition to return immediately. --- ...unctions-map-distributed-stack.assets.json | 19 + ...ctions-map-distributed-stack.template.json | 216 +++++++++ .../cdk.out | 1 + ...efaultTestDeployAssert63593303.assets.json | 19 + ...aultTestDeployAssert63593303.template.json | 36 ++ .../integ.json | 12 + .../manifest.json | 167 +++++++ .../tree.json | 429 ++++++++++++++++++ .../test/integ.map-distributed-iam.ts | 59 +++ .../aws-stepfunctions/lib/state-graph.ts | 21 +- .../aws-stepfunctions/lib/states/state.ts | 53 +++ .../test/state-graph.test.ts | 76 ++++ 12 files changed, 1090 insertions(+), 18 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.ts create mode 100644 packages/aws-cdk-lib/aws-stepfunctions/test/state-graph.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.assets.json new file mode 100644 index 0000000000000..1f7bf86de2cc8 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "c583542f1a1a4d70c5b7930dda94ecccba23a2fcb848dad95e5812cc61d18d1f": { + "source": { + "path": "cdk-stepfunctions-map-distributed-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "c583542f1a1a4d70c5b7930dda94ecccba23a2fcb848dad95e5812cc61d18d1f.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.template.json new file mode 100644 index 0000000000000..6fad197e0eba2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk-stepfunctions-map-distributed-stack.template.json @@ -0,0 +1,216 @@ +{ + "Resources": { + "StateMachine1RoleDE82F282": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine18AFC9B86": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": "{\"StartAt\":\"My-Map-State\",\"States\":{\"My-Map-State\":{\"Type\":\"Map\",\"End\":true,\"ItemsPath\":\"$.inputForMap\",\"ItemSelector\":{\"foo\":\"foo\",\"bar.$\":\"$.bar\"},\"ItemProcessor\":{\"ProcessorConfig\":{\"Mode\":\"DISTRIBUTED\",\"ExecutionType\":\"STANDARD\"},\"StartAt\":\"Pass State 1\",\"States\":{\"Pass State 1\":{\"Type\":\"Pass\",\"End\":true}}},\"MaxConcurrencyPath\":\"$.maxConcurrency\"}},\"TimeoutSeconds\":30}", + "RoleArn": { + "Fn::GetAtt": [ + "StateMachine1RoleDE82F282", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachine1RoleDE82F282" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "StateMachine1DistributedMapPolicyA6BF4F8F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine18AFC9B86" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Ref": "StateMachine18AFC9B86" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachine1DistributedMapPolicyA6BF4F8F", + "Roles": [ + { + "Ref": "StateMachine1RoleDE82F282" + } + ] + } + }, + "StateMachine2Role6BE3CF0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine21CE8E3CE": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": "{\"StartAt\":\"My-Map-State\",\"States\":{\"My-Map-State\":{\"Type\":\"Map\",\"End\":true,\"ItemsPath\":\"$.inputForMap\",\"ItemSelector\":{\"foo\":\"foo\",\"bar.$\":\"$.bar\"},\"ItemProcessor\":{\"ProcessorConfig\":{\"Mode\":\"DISTRIBUTED\",\"ExecutionType\":\"STANDARD\"},\"StartAt\":\"Pass State 2\",\"States\":{\"Pass State 2\":{\"Type\":\"Pass\",\"End\":true}}},\"MaxConcurrencyPath\":\"$.maxConcurrency\"}},\"TimeoutSeconds\":30}", + "RoleArn": { + "Fn::GetAtt": [ + "StateMachine2Role6BE3CF0B", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachine2Role6BE3CF0B" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "StateMachine2DistributedMapPolicyECDEB23C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine21CE8E3CE" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Ref": "StateMachine21CE8E3CE" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachine2DistributedMapPolicyECDEB23C", + "Roles": [ + { + "Ref": "StateMachine2Role6BE3CF0B" + } + ] + } + } + }, + "Outputs": { + "StateMachine1ARN": { + "Value": { + "Ref": "StateMachine18AFC9B86" + } + }, + "StateMachine1RoleARN": { + "Value": { + "Fn::GetAtt": [ + "StateMachine1RoleDE82F282", + "Arn" + ] + } + }, + "StateMachine2ARN": { + "Value": { + "Ref": "StateMachine21CE8E3CE" + } + }, + "StateMachine2RoleARN": { + "Value": { + "Fn::GetAtt": [ + "StateMachine2Role6BE3CF0B", + "Arn" + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets.json new file mode 100644 index 0000000000000..ce8984756f21c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/integ.json new file mode 100644 index 0000000000000..0615a23b2885d --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest": { + "stacks": [ + "cdk-stepfunctions-map-distributed-stack" + ], + "assertionStack": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert", + "assertionStackName": "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/manifest.json new file mode 100644 index 0000000000000..92798db835ee8 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/manifest.json @@ -0,0 +1,167 @@ +{ + "version": "36.0.0", + "artifacts": { + "cdk-stepfunctions-map-distributed-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdk-stepfunctions-map-distributed-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdk-stepfunctions-map-distributed-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdk-stepfunctions-map-distributed-stack.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c583542f1a1a4d70c5b7930dda94ecccba23a2fcb848dad95e5812cc61d18d1f.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdk-stepfunctions-map-distributed-stack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdk-stepfunctions-map-distributed-stack.assets" + ], + "metadata": { + "/cdk-stepfunctions-map-distributed-stack/StateMachine1/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine1RoleDE82F282" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine1/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine18AFC9B86" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine1/DistributedMapPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine1DistributedMapPolicyA6BF4F8F" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine1ARN": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine1ARN" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine1RoleARN": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine1RoleARN" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine2/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine2Role6BE3CF0B" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine2/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine21CE8E3CE" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine2/DistributedMapPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine2DistributedMapPolicyECDEB23C" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine2ARN": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine2ARN" + } + ], + "/cdk-stepfunctions-map-distributed-stack/StateMachine2RoleARN": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine2RoleARN" + } + ], + "/cdk-stepfunctions-map-distributed-stack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-stepfunctions-map-distributed-stack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-stepfunctions-map-distributed-stack" + }, + "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdkstepfunctionsmapdistributediamintegDefaultTestDeployAssert63593303.assets" + ], + "metadata": { + "/cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/tree.json new file mode 100644 index 0000000000000..66c52161519a0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.js.snapshot/tree.json @@ -0,0 +1,429 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "cdk-stepfunctions-map-distributed-stack": { + "id": "cdk-stepfunctions-map-distributed-stack", + "path": "cdk-stepfunctions-map-distributed-stack", + "children": { + "Map 1": { + "id": "Map 1", + "path": "cdk-stepfunctions-map-distributed-stack/Map 1", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.DistributedMap", + "version": "0.0.0" + } + }, + "Pass State 1": { + "id": "Pass State 1", + "path": "cdk-stepfunctions-map-distributed-stack/Pass State 1", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.Pass", + "version": "0.0.0" + } + }, + "StateMachine1": { + "id": "StateMachine1", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1", + "children": { + "Role": { + "id": "Role", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/Role/ImportRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "definitionString": "{\"StartAt\":\"My-Map-State\",\"States\":{\"My-Map-State\":{\"Type\":\"Map\",\"End\":true,\"ItemsPath\":\"$.inputForMap\",\"ItemSelector\":{\"foo\":\"foo\",\"bar.$\":\"$.bar\"},\"ItemProcessor\":{\"ProcessorConfig\":{\"Mode\":\"DISTRIBUTED\",\"ExecutionType\":\"STANDARD\"},\"StartAt\":\"Pass State 1\",\"States\":{\"Pass State 1\":{\"Type\":\"Pass\",\"End\":true}}},\"MaxConcurrencyPath\":\"$.maxConcurrency\"}},\"TimeoutSeconds\":30}", + "roleArn": { + "Fn::GetAtt": [ + "StateMachine1RoleDE82F282", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + }, + "DistributedMapPolicy": { + "id": "DistributedMapPolicy", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/DistributedMapPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1/DistributedMapPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine18AFC9B86" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Ref": "StateMachine18AFC9B86" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachine1DistributedMapPolicyA6BF4F8F", + "roles": [ + { + "Ref": "StateMachine1RoleDE82F282" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "StateMachine1ARN": { + "id": "StateMachine1ARN", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1ARN", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "StateMachine1RoleARN": { + "id": "StateMachine1RoleARN", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine1RoleARN", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "Map 2": { + "id": "Map 2", + "path": "cdk-stepfunctions-map-distributed-stack/Map 2", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.DistributedMap", + "version": "0.0.0" + } + }, + "Pass State 2": { + "id": "Pass State 2", + "path": "cdk-stepfunctions-map-distributed-stack/Pass State 2", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.Pass", + "version": "0.0.0" + } + }, + "StateMachine2": { + "id": "StateMachine2", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2", + "children": { + "Role": { + "id": "Role", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/Role/ImportRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "definitionString": "{\"StartAt\":\"My-Map-State\",\"States\":{\"My-Map-State\":{\"Type\":\"Map\",\"End\":true,\"ItemsPath\":\"$.inputForMap\",\"ItemSelector\":{\"foo\":\"foo\",\"bar.$\":\"$.bar\"},\"ItemProcessor\":{\"ProcessorConfig\":{\"Mode\":\"DISTRIBUTED\",\"ExecutionType\":\"STANDARD\"},\"StartAt\":\"Pass State 2\",\"States\":{\"Pass State 2\":{\"Type\":\"Pass\",\"End\":true}}},\"MaxConcurrencyPath\":\"$.maxConcurrency\"}},\"TimeoutSeconds\":30}", + "roleArn": { + "Fn::GetAtt": [ + "StateMachine2Role6BE3CF0B", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + }, + "DistributedMapPolicy": { + "id": "DistributedMapPolicy", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/DistributedMapPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2/DistributedMapPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine21CE8E3CE" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Ref": "StateMachine21CE8E3CE" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachine2DistributedMapPolicyECDEB23C", + "roles": [ + { + "Ref": "StateMachine2Role6BE3CF0B" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "StateMachine2ARN": { + "id": "StateMachine2ARN", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2ARN", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "StateMachine2RoleARN": { + "id": "StateMachine2RoleARN", + "path": "cdk-stepfunctions-map-distributed-stack/StateMachine2RoleARN", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-stepfunctions-map-distributed-stack/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-stepfunctions-map-distributed-stack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "cdk-stepfunctions-map-distributed-iam-integ": { + "id": "cdk-stepfunctions-map-distributed-iam-integ", + "path": "cdk-stepfunctions-map-distributed-iam-integ", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-stepfunctions-map-distributed-iam-integ/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.ts new file mode 100644 index 0000000000000..488ced0ef4270 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-stepfunctions/test/integ.map-distributed-iam.ts @@ -0,0 +1,59 @@ +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as cdk from 'aws-cdk-lib'; +import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; + +/** + * Stack verification steps: + * + * -- aws stepfunctions describe-state-machine --state-machine-arn has a status of `ACTIVE` + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'cdk-stepfunctions-map-distributed-stack'); +let index = 0; + +function createMap() { + index++; + + return new sfn.DistributedMap(stack, `Map ${index}`, { + stateName: 'My-Map-State', + maxConcurrencyPath: sfn.JsonPath.stringAt('$.maxConcurrency'), + itemsPath: sfn.JsonPath.stringAt('$.inputForMap'), + itemSelector: { + foo: 'foo', + bar: sfn.JsonPath.stringAt('$.bar'), + }, + }).itemProcessor(new sfn.Pass(stack, `Pass State ${index}`), { + mode: sfn.ProcessorMode.DISTRIBUTED, + executionType: sfn.ProcessorType.STANDARD, + }); +} + +const sm1 = new sfn.StateMachine(stack, 'StateMachine1', { + definitionBody: sfn.DefinitionBody.fromChainable(createMap()), + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'StateMachine1ARN', { + value: sm1.stateMachineArn, +}); +new cdk.CfnOutput(stack, 'StateMachine1RoleARN', { + value: sm1.role.roleArn, +}); + +const sm2 = new sfn.StateMachine(stack, 'StateMachine2', { + definitionBody: sfn.DefinitionBody.fromChainable(createMap()), + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'StateMachine2ARN', { + value: sm2.stateMachineArn, +}); +new cdk.CfnOutput(stack, 'StateMachine2RoleARN', { + value: sm2.role.roleArn, +}); + +new IntegTest(app, 'cdk-stepfunctions-map-distributed-iam-integ', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts index 364b43064883a..7d721fcbba90d 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts @@ -166,24 +166,9 @@ export class StateGraph { */ public bind(stateMachine: StateMachine) { for (const state of this.allStates) { - if (DistributedMap.isDistributedMap(state)) { - stateMachine.role.attachInlinePolicy(new iam.Policy(stateMachine, 'DistributedMapPolicy', { - document: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: ['states:StartExecution'], - resources: [stateMachine.stateMachineArn], - }), - new iam.PolicyStatement({ - actions: ['states:DescribeExecution', 'states:StopExecution'], - resources: [`${stateMachine.stateMachineArn}:*`], - }), - ], - }), - })); - - break; - } + state.bindToStateMachine(stateMachine, { + isDistributedMap: DistributedMap.isDistributedMap, + }); } } } diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts index 0ef1e394a29ee..3cd7c901a88df 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts @@ -1,10 +1,26 @@ import { IConstruct, Construct, Node } from 'constructs'; +import * as iam from '../../../aws-iam'; import { Token } from '../../../core'; import { Condition } from '../condition'; import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; +import { StateMachine } from '../state-machine'; import { CatchProps, Errors, IChainable, INextable, ProcessorConfig, ProcessorMode, RetryProps } from '../types'; +/** + * Options for state machine binding + */ +export interface IBindToStateMachineProps { + /** + * We create a cyclic dependency if we include a call to DistributedMap.isDistributedMap in State. Instead, accept + * it as an option for bindToStateMachine. + * + * @param x the element to check if it is a distributed map. + * @internal + */ + isDistributedMap(x: any): boolean; +} + /** * Properties shared by all states */ @@ -277,6 +293,43 @@ export abstract class State extends Construct implements IChainable { } } + /** + * Binds this state to the provided state machine. If there are any branches, also binds them. + * + * @param stateMachine the state machine to bind to [disable-awslint:ref-via-interface] + * @param props bind options (currently this only includes a function for determining if isDistributedMap) + * @internal + */ + public bindToStateMachine(stateMachine: StateMachine, props: IBindToStateMachineProps) { + if (stateMachine.role.node.tryFindChild('DistributedMapPolicy')) { + return; + } + + if (props.isDistributedMap(this)) { + stateMachine.role.attachInlinePolicy(new iam.Policy(stateMachine, 'DistributedMapPolicy', { + document: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['states:StartExecution'], + resources: [stateMachine.stateMachineArn], + }), + new iam.PolicyStatement({ + actions: ['states:DescribeExecution', 'states:StopExecution'], + resources: [`${stateMachine.stateMachineArn}:*`], + }), + ], + }), + })); + + return; + } + + // nothing here, let's check the branches. + for (const graph of this.branches) { + graph.bind(stateMachine); + } + } + /** * Render the state as JSON */ diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/state-graph.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/state-graph.test.ts new file mode 100644 index 0000000000000..5781f136b1ee9 --- /dev/null +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/state-graph.test.ts @@ -0,0 +1,76 @@ +import * as assertions from '../../assertions'; +import * as cdk from '../../core'; +import * as stepfunctions from '../lib'; + +describe('State Graph', () => { + test('bind adds execution permissions to state machine when distributed map is used within the primary graph', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const map = createMap(stack); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definitionBody: stepfunctions.DefinitionBody.fromChainable(map), + }); + const stateMachineLogicalId = stack.getLogicalId(stateMachine.node.defaultChild as stepfunctions.CfnStateMachine); + const template = assertions.Template.fromStack(stack); + + // THEN + template.hasResource('AWS::IAM::Policy', createPolicyProps(stateMachineLogicalId)); + }); + + test('bind adds execution permissions to state machine when distributed map is used within a child graph', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const map = createMap(stack); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definitionBody: stepfunctions.DefinitionBody.fromChainable(new stepfunctions.Parallel(stack, 'Parallel', { + resultPath: '$.result', + }).branch( + map, + )), + }); + const stateMachineLogicalId = stack.getLogicalId(stateMachine.node.defaultChild as stepfunctions.CfnStateMachine); + const template = assertions.Template.fromStack(stack); + + // THEN + template.hasResource('AWS::IAM::Policy', createPolicyProps(stateMachineLogicalId)); + }); +}); + +function createMap(stack: cdk.Stack) { + return new stepfunctions.DistributedMap(stack, 'Map', { + maxConcurrency: 1, + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), + itemSelector: { + foo: 'foo', + bar: stepfunctions.JsonPath.stringAt('$.bar'), + }, + }).itemProcessor(new stepfunctions.Pass(this, 'Pass State')); +} + +function createPolicyProps(stateMachineLogicalId: string) { + return { + Properties: { + PolicyDocument: { + // ensure that self-starting permission is added which is necessary for distributed maps + Statement: [ + { + Action: 'states:StartExecution', + Resource: { + Ref: stateMachineLogicalId, + }, + }, + { + Action: ['states:DescribeExecution', 'states:StopExecution'], + Resource: { + 'Fn::Join': ['', [{ Ref: stateMachineLogicalId }, ':*']], + }, + }, + ], + }, + }, + }; +} \ No newline at end of file