diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index e4ae99f92f423..5e98aee6775b3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -577,6 +577,34 @@ const definition = sfn.Chain // ... ``` +## Task Credentials + +Tasks are executed using the State Machine's execution role. In some cases, e.g. cross-account access, an IAM role can be assumed by the State Machine's execution role to provide access to the resource. +This can be achieved by providing the optional `credentials` property which allows using a fixed role or a json expression to resolve the role at runtime from the task's inputs. + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const submitLambda: lambda.Function; +declare const iamRole: iam.Role; + +// use a fixed role for all task invocations +const role = sfn.TaskRole.fromRole(iamRole); +// or use a json expression to resolve the role at runtime based on task inputs +//const role = sfn.TaskRole.fromRoleArnJsonPath('$.RoleArn'); + +const submitJob = new tasks.LambdaInvoke(this, 'Submit Job', { + lambdaFunction: submitLambda, + outputPath: '$.Payload', + // use credentials + credentials: { role }, +}); +``` + +See [the AWS documentation](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-access-cross-acct-resources.html) +to learn more about AWS Step Functions support for accessing resources in other AWS accounts. + ## State Machine Fragments It is possible to define reusable (or abstracted) mini-state machines by diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index 930e988b5f838..0a2d693f0dbe5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -22,6 +22,7 @@ export * from './states/map'; export * from './states/custom-state'; export * from './states/task-base'; +export * from './task-credentials'; // AWS::StepFunctions CloudFormation Resources: export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts index 4009c27328fa1..3b933bc4a8b53 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts @@ -3,7 +3,9 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Chain } from '../chain'; +import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; +import { Credentials } from '../task-credentials'; import { CatchProps, IChainable, INextable, RetryProps } from '../types'; import { renderJsonPath, State } from './state'; @@ -91,6 +93,16 @@ export interface TaskStateBaseProps { * */ readonly integrationPattern?: IntegrationPattern; + + /** + * Credentials for an IAM Role that the State Machine assumes for executing the task. + * This enables cross-account resource invocations. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-access-cross-acct-resources.html + * + * @default - None (Task is executed using the State Machine's execution role) + */ + readonly credentials?: Credentials; } /** @@ -112,12 +124,14 @@ export abstract class TaskStateBase extends State implements INextable { private readonly timeout?: cdk.Duration; private readonly heartbeat?: cdk.Duration; + private readonly credentials?: Credentials; constructor(scope: Construct, id: string, props: TaskStateBaseProps) { super(scope, id, props); this.endStates = [this]; this.timeout = props.timeout; this.heartbeat = props.heartbeat; + this.credentials = props.credentials; } /** @@ -263,6 +277,13 @@ export abstract class TaskStateBase extends State implements INextable { for (const policyStatement of this.taskPolicies || []) { graph.registerPolicyStatement(policyStatement); } + if (this.credentials) { + graph.registerPolicyStatement(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['sts:AssumeRole'], + resources: [this.credentials.role.resource], + })); + } } /** @@ -277,6 +298,10 @@ export abstract class TaskStateBase extends State implements INextable { return this.metric(prefix + suffix, props); } + private renderCredentials() { + return this.credentials ? FieldUtils.renderObject({ Credentials: { RoleArn: this.credentials.role.roleArn } }) : undefined; + } + private renderTaskBase() { return { Type: 'Task', @@ -287,6 +312,7 @@ export abstract class TaskStateBase extends State implements INextable { OutputPath: renderJsonPath(this.outputPath), ResultPath: renderJsonPath(this.resultPath), ...this.renderResultSelector(), + ...this.renderCredentials(), }; } } @@ -347,4 +373,4 @@ export enum IntegrationPattern { * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token */ WAIT_FOR_TASK_TOKEN = 'WAIT_FOR_TASK_TOKEN' -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/task-credentials.ts b/packages/@aws-cdk/aws-stepfunctions/lib/task-credentials.ts new file mode 100644 index 0000000000000..391589b43882d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/task-credentials.ts @@ -0,0 +1,77 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { JsonPath } from './fields'; + +/** + * Specifies a target role assumed by the State Machine's execution role for invoking the task's resource. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-access-cross-acct-resources.html + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-task-state.html#task-state-fields + */ +export interface Credentials { + /** + * The role to be assumed for executing the Task. + */ + readonly role: TaskRole; +} + +/** + * Role to be assumed by the State Machine's execution role for invoking a task's resource. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-access-cross-acct-resources.html + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-task-state.html#task-state-fields + */ +export abstract class TaskRole { + /** + * Construct a task role retrieved from task inputs using a json expression + * + * @param expression json expression to roleArn + * + * @example + * + * TaskRole.fromRoleArnJsonPath('$.RoleArn'); + */ + public static fromRoleArnJsonPath(expression: string): TaskRole { + return new JsonExpressionTaskRole(expression); + } + + /** + * Construct a task role based on the provided IAM Role + * + * @param role IAM Role + */ + public static fromRole(role: iam.IRole): TaskRole { + return new IamRoleTaskRole(role); + } + + /** + * Retrieves the roleArn for this TaskRole + */ + public abstract readonly roleArn: string; + + /** + * Retrieves the resource for use in IAM Policies for this TaskRole + */ + public abstract readonly resource: string; +} + +class JsonExpressionTaskRole extends TaskRole { + public readonly resource: string; + public readonly roleArn: string; + + constructor(expression: string) { + super(); + this.roleArn = JsonPath.stringAt(expression); + this.resource = '*'; + } +} + +class IamRoleTaskRole extends TaskRole { + public readonly resource: string; + public readonly roleArn: string; + + constructor(role: iam.IRole) { + super(); + this.roleArn = role.roleArn; + this.resource = role.roleArn; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets.json new file mode 100644 index 0000000000000..b2b7f9ec32cf9 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.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/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.template.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.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/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.assets.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.assets.json new file mode 100644 index 0000000000000..d49d5e0741c6a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "d775f19c6469457d54fcd62837c1d84ec75c1b8aea7b635bb10dc74dcc0e474d": { + "source": { + "path": "aws-stepfunctions-state-machine-credentials-integ.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d775f19c6469457d54fcd62837c1d84ec75c1b8aea7b635bb10dc74dcc0e474d.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/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.template.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.template.json new file mode 100644 index 0000000000000..b4004045bfbcd --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/aws-stepfunctions-state-machine-credentials-integ.template.json @@ -0,0 +1,251 @@ +{ + "Resources": { + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineWithLiteralCredentialsRole1F1DEEC1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineWithLiteralCredentialsRoleDefaultPolicy331008EE": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineWithLiteralCredentialsRoleDefaultPolicy331008EE", + "Roles": [ + { + "Ref": "StateMachineWithLiteralCredentialsRole1F1DEEC1" + } + ] + } + }, + "StateMachineWithLiteralCredentialsBF5A67AE": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineWithLiteralCredentialsRole1F1DEEC1", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"FakeTaskWithLiteralCredentials\",\"States\":{\"FakeTaskWithLiteralCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn\":\"", + { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + }, + "\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineWithLiteralCredentialsRoleDefaultPolicy331008EE", + "StateMachineWithLiteralCredentialsRole1F1DEEC1" + ] + }, + "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineWithCrossAccountLiteralCredentialsRoleDefaultPolicy9B9943BD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "arn:aws:iam::123456789012:role/CrossAccountRole" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineWithCrossAccountLiteralCredentialsRoleDefaultPolicy9B9943BD", + "Roles": [ + { + "Ref": "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC" + } + ] + } + }, + "StateMachineWithCrossAccountLiteralCredentialsA2DD713D": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"FakeTaskWithCrossAccountLiteralCredentials\",\"States\":{\"FakeTaskWithCrossAccountLiteralCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn\":\"arn:aws:iam::123456789012:role/CrossAccountRole\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + }, + "DependsOn": [ + "StateMachineWithCrossAccountLiteralCredentialsRoleDefaultPolicy9B9943BD", + "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC" + ] + }, + "StateMachineWithJsonPathCredentialsRole7BDE9FA6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineWithJsonPathCredentialsRoleDefaultPolicy1DA1C50B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineWithJsonPathCredentialsRoleDefaultPolicy1DA1C50B", + "Roles": [ + { + "Ref": "StateMachineWithJsonPathCredentialsRole7BDE9FA6" + } + ] + } + }, + "StateMachineWithJsonPathCredentials5786712E": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineWithJsonPathCredentialsRole7BDE9FA6", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"FakeTaskWithJsonPathCredentials\",\"States\":{\"FakeTaskWithJsonPathCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn.$\":\"$.RoleArn\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + }, + "DependsOn": [ + "StateMachineWithJsonPathCredentialsRoleDefaultPolicy1DA1C50B", + "StateMachineWithJsonPathCredentialsRole7BDE9FA6" + ] + } + }, + "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/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/cdk.out b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/integ.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/integ.json new file mode 100644 index 0000000000000..2dee27641051b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "StateMachineCredentials/DefaultTest": { + "stacks": [ + "aws-stepfunctions-state-machine-credentials-integ" + ], + "assertionStack": "StateMachineCredentials/DefaultTest/DeployAssert", + "assertionStackName": "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/manifest.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/manifest.json new file mode 100644 index 0000000000000..fbf770c2e4d80 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/manifest.json @@ -0,0 +1,165 @@ +{ + "version": "21.0.0", + "artifacts": { + "aws-stepfunctions-state-machine-credentials-integ.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-stepfunctions-state-machine-credentials-integ.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-stepfunctions-state-machine-credentials-integ": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-stepfunctions-state-machine-credentials-integ.template.json", + "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}/d775f19c6469457d54fcd62837c1d84ec75c1b8aea7b635bb10dc74dcc0e474d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-stepfunctions-state-machine-credentials-integ.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": [ + "aws-stepfunctions-state-machine-credentials-integ.assets" + ], + "metadata": { + "/aws-stepfunctions-state-machine-credentials-integ/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Role1ABCC5F0" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithLiteralCredentialsRole1F1DEEC1" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithLiteralCredentialsRoleDefaultPolicy331008EE" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithLiteralCredentialsBF5A67AE" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithCrossAccountLiteralCredentialsRoleDefaultPolicy9B9943BD" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithCrossAccountLiteralCredentialsA2DD713D" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithJsonPathCredentialsRole7BDE9FA6" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithJsonPathCredentialsRoleDefaultPolicy1DA1C50B" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineWithJsonPathCredentials5786712E" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-stepfunctions-state-machine-credentials-integ/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-stepfunctions-state-machine-credentials-integ" + }, + "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.template.json", + "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": [ + "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.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": [ + "StateMachineCredentialsDefaultTestDeployAssert3F5E6D8D.assets" + ], + "metadata": { + "/StateMachineCredentials/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/StateMachineCredentials/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "StateMachineCredentials/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/tree.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/tree.json new file mode 100644 index 0000000000000..c01706db5b425 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.js.snapshot/tree.json @@ -0,0 +1,546 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-stepfunctions-state-machine-credentials-integ": { + "id": "aws-stepfunctions-state-machine-credentials-integ", + "path": "aws-stepfunctions-state-machine-credentials-integ", + "children": { + "Role": { + "id": "Role", + "path": "aws-stepfunctions-state-machine-credentials-integ/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-stepfunctions-state-machine-credentials-integ/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "FakeTaskWithLiteralCredentials": { + "id": "FakeTaskWithLiteralCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/FakeTaskWithLiteralCredentials", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.TaskStateBase", + "version": "0.0.0" + } + }, + "StateMachineWithLiteralCredentials": { + "id": "StateMachineWithLiteralCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials", + "children": { + "Role": { + "id": "Role", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/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/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachineWithLiteralCredentialsRoleDefaultPolicy331008EE", + "roles": [ + { + "Ref": "StateMachineWithLiteralCredentialsRole1F1DEEC1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithLiteralCredentials/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "roleArn": { + "Fn::GetAtt": [ + "StateMachineWithLiteralCredentialsRole1F1DEEC1", + "Arn" + ] + }, + "definitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"FakeTaskWithLiteralCredentials\",\"States\":{\"FakeTaskWithLiteralCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn\":\"", + { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + }, + "\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + ] + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "CrossAccountRole": { + "id": "CrossAccountRole", + "path": "aws-stepfunctions-state-machine-credentials-integ/CrossAccountRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "FakeTaskWithCrossAccountLiteralCredentials": { + "id": "FakeTaskWithCrossAccountLiteralCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/FakeTaskWithCrossAccountLiteralCredentials", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.TaskStateBase", + "version": "0.0.0" + } + }, + "StateMachineWithCrossAccountLiteralCredentials": { + "id": "StateMachineWithCrossAccountLiteralCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials", + "children": { + "Role": { + "id": "Role", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/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/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "arn:aws:iam::123456789012:role/CrossAccountRole" + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachineWithCrossAccountLiteralCredentialsRoleDefaultPolicy9B9943BD", + "roles": [ + { + "Ref": "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithCrossAccountLiteralCredentials/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "roleArn": { + "Fn::GetAtt": [ + "StateMachineWithCrossAccountLiteralCredentialsRole4AA04DBC", + "Arn" + ] + }, + "definitionString": "{\"StartAt\":\"FakeTaskWithCrossAccountLiteralCredentials\",\"States\":{\"FakeTaskWithCrossAccountLiteralCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn\":\"arn:aws:iam::123456789012:role/CrossAccountRole\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "FakeTaskWithJsonPathCredentials": { + "id": "FakeTaskWithJsonPathCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/FakeTaskWithJsonPathCredentials", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.TaskStateBase", + "version": "0.0.0" + } + }, + "StateMachineWithJsonPathCredentials": { + "id": "StateMachineWithJsonPathCredentials", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials", + "children": { + "Role": { + "id": "Role", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/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/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachineWithJsonPathCredentialsRoleDefaultPolicy1DA1C50B", + "roles": [ + { + "Ref": "StateMachineWithJsonPathCredentialsRole7BDE9FA6" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-state-machine-credentials-integ/StateMachineWithJsonPathCredentials/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "roleArn": { + "Fn::GetAtt": [ + "StateMachineWithJsonPathCredentialsRole7BDE9FA6", + "Arn" + ] + }, + "definitionString": "{\"StartAt\":\"FakeTaskWithJsonPathCredentials\",\"States\":{\"FakeTaskWithJsonPathCredentials\":{\"End\":true,\"Type\":\"Task\",\"Credentials\":{\"RoleArn.$\":\"$.RoleArn\"},\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}}}},\"TimeoutSeconds\":30}" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-stepfunctions-state-machine-credentials-integ/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-stepfunctions-state-machine-credentials-integ/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "StateMachineCredentials": { + "id": "StateMachineCredentials", + "path": "StateMachineCredentials", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "StateMachineCredentials/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "StateMachineCredentials/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "StateMachineCredentials/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "StateMachineCredentials/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "StateMachineCredentials/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.ts new file mode 100644 index 0000000000000..a5e0b6f584bd1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine-credentials.ts @@ -0,0 +1,70 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as constructs from 'constructs'; +import * as sfn from '../lib'; + +export interface FakeTaskProps extends sfn.TaskStateBaseProps { + parameters?: { [key: string]: string }; +} + +/** + * Task extending sfn.TaskStateBase to facilitate integ testing setting credentials + */ +export class FakeTask extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly parameters?: { [key: string]: string }; + + constructor(scope: constructs.Construct, id: string, props: FakeTaskProps = {}) { + super(scope, id, props); + this.parameters = props.parameters; + } + + protected _renderTask(): any { + return { + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:putItem', + Parameters: { + TableName: 'my-cool-table', + Item: { + id: { + S: 'my-entry', + }, + }, + ...this.parameters, + }, + }; + } +} + +/* + * 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, 'aws-stepfunctions-state-machine-credentials-integ'); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountPrincipal(stack.account), +}); + +new sfn.StateMachine(stack, 'StateMachineWithLiteralCredentials', { + definition: new FakeTask(stack, 'FakeTaskWithLiteralCredentials', { credentials: { role: sfn.TaskRole.fromRole(role) } }), + timeout: cdk.Duration.seconds(30), +}); + +const crossAccountRole = iam.Role.fromRoleArn(stack, 'CrossAccountRole', 'arn:aws:iam::123456789012:role/CrossAccountRole'); + +new sfn.StateMachine(stack, 'StateMachineWithCrossAccountLiteralCredentials', { + definition: new FakeTask(stack, 'FakeTaskWithCrossAccountLiteralCredentials', { credentials: { role: sfn.TaskRole.fromRole(crossAccountRole) } }), + timeout: cdk.Duration.seconds(30), +}); + +new sfn.StateMachine(stack, 'StateMachineWithJsonPathCredentials', { + definition: new FakeTask(stack, 'FakeTaskWithJsonPathCredentials', { credentials: { role: sfn.TaskRole.fromRoleArnJsonPath('$.RoleArn') } }), + timeout: cdk.Duration.seconds(30), +}); + +new IntegTest(app, 'StateMachineCredentials', { testCases: [stack] }); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/fake-task.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/fake-task.ts new file mode 100644 index 0000000000000..01a0c9bfa0da1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/fake-task.ts @@ -0,0 +1,29 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as constructs from 'constructs'; +import * as sfn from '../../lib'; + +export interface FakeTaskProps extends sfn.TaskStateBaseProps { + readonly metrics?: sfn.TaskMetricsConfig; +} + +export class FakeTask extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: constructs.Construct, id: string, props: FakeTaskProps = {}) { + super(scope, id, props); + this.taskMetrics = props.metrics; + } + + /** + * @internal + */ + protected _renderTask(): any { + return { + Resource: 'my-resource', + Parameters: sfn.FieldUtils.renderObject({ + MyParameter: 'myParameter', + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts index ceb8998c7da92..ee1095bfd5e66 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts @@ -9,4 +9,8 @@ import * as sfn from '../../lib'; */ export function render(stack: cdk.Stack, definition: sfn.IChainable) { return stack.resolve(new sfn.StateGraph(definition.startState, 'Test Graph').toGraphJson()); -} \ No newline at end of file +} + +export function renderGraph(definition: sfn.IChainable) { + return render(new cdk.Stack(), definition); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index 6846f112f4d34..f99c9cd41e277 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -1,8 +1,10 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import * as stepfunctions from '../lib'; +import * as sfn from '../lib'; +import { FakeTask } from './private/fake-task'; describe('State Machine', () => { test('Instantiate Default State Machine', () => { @@ -10,9 +12,9 @@ describe('State Machine', () => { const stack = new cdk.Stack(); // WHEN - new stepfunctions.StateMachine(stack, 'MyStateMachine', { + new sfn.StateMachine(stack, 'MyStateMachine', { stateMachineName: 'MyStateMachine', - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), }); // THEN @@ -27,10 +29,10 @@ describe('State Machine', () => { const stack = new cdk.Stack(); // WHEN - new stepfunctions.StateMachine(stack, 'MyStateMachine', { + new sfn.StateMachine(stack, 'MyStateMachine', { stateMachineName: 'MyStateMachine', - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), - stateMachineType: stepfunctions.StateMachineType.STANDARD, + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), + stateMachineType: sfn.StateMachineType.STANDARD, }); // THEN @@ -47,10 +49,10 @@ describe('State Machine', () => { const stack = new cdk.Stack(); // WHEN - new stepfunctions.StateMachine(stack, 'MyStateMachine', { + new sfn.StateMachine(stack, 'MyStateMachine', { stateMachineName: 'MyStateMachine', - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), - stateMachineType: stepfunctions.StateMachineType.EXPRESS, + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), + stateMachineType: sfn.StateMachineType.EXPRESS, }); // THEN @@ -68,10 +70,10 @@ describe('State Machine', () => { // WHEN const createStateMachine = (name: string) => { - new stepfunctions.StateMachine(stack, name + 'StateMachine', { + new sfn.StateMachine(stack, name + 'StateMachine', { stateMachineName: name, - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, name + 'Pass')), - stateMachineType: stepfunctions.StateMachineType.EXPRESS, + definition: sfn.Chain.start(new sfn.Pass(stack, name + 'Pass')), + stateMachineType: sfn.StateMachineType.EXPRESS, }); }; const tooShortName = ''; @@ -95,8 +97,8 @@ describe('State Machine', () => { test('State Machine with valid name', () => { // GIVEN const stack = new cdk.Stack(); - const newStateMachine = new stepfunctions.StateMachine(stack, 'dummyStateMachineToken', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'dummyStateMachineTokenPass')), + const newStateMachine = new sfn.StateMachine(stack, 'dummyStateMachineToken', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'dummyStateMachineTokenPass')), }); // WHEN @@ -105,18 +107,18 @@ describe('State Machine', () => { // THEN expect(() => { - new stepfunctions.StateMachine(stack, 'TokenTest-StateMachine', { + new sfn.StateMachine(stack, 'TokenTest-StateMachine', { stateMachineName: nameContainingToken, - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'TokenTest-StateMachinePass')), - stateMachineType: stepfunctions.StateMachineType.EXPRESS, + definition: sfn.Chain.start(new sfn.Pass(stack, 'TokenTest-StateMachinePass')), + stateMachineType: sfn.StateMachineType.EXPRESS, }); }).not.toThrow(); expect(() => { - new stepfunctions.StateMachine(stack, 'ValidNameTest-StateMachine', { + new sfn.StateMachine(stack, 'ValidNameTest-StateMachine', { stateMachineName: validName, - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'ValidNameTest-StateMachinePass')), - stateMachineType: stepfunctions.StateMachineType.EXPRESS, + definition: sfn.Chain.start(new sfn.Pass(stack, 'ValidNameTest-StateMachinePass')), + stateMachineType: sfn.StateMachineType.EXPRESS, }); }).not.toThrow(); }); @@ -128,11 +130,11 @@ describe('State Machine', () => { // WHEN const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); - new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), + new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), logs: { destination: logGroup, - level: stepfunctions.LogLevel.FATAL, + level: sfn.LogLevel.FATAL, includeExecutionData: false, }, }); @@ -185,8 +187,8 @@ describe('State Machine', () => { const stack = new cdk.Stack(); // WHEN - new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), + new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true, }); @@ -226,8 +228,8 @@ describe('State Machine', () => { const stack = new cdk.Stack(); // WHEN - const sm = new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), + const sm = new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), }); const bucket = new s3.Bucket(stack, 'MyBucket'); bucket.grantRead(sm); @@ -278,6 +280,164 @@ describe('State Machine', () => { }); }); + test('Instantiate a State Machine with a task assuming a literal roleArn (cross-account)', () => { + // GIVEN + const app = new cdk.App(); + const stateMachineStack = new cdk.Stack(app, 'StateMachineStack', { env: { account: '123456789' } }); + const roleStack = new cdk.Stack(app, 'RoleStack', { env: { account: '987654321' } }); + const role = iam.Role.fromRoleName(roleStack, 'Role', 'example-role'); + + // WHEN + new sfn.StateMachine(stateMachineStack, 'MyStateMachine', { + definition: new FakeTask(stateMachineStack, 'fakeTask', { credentials: { role: sfn.TaskRole.fromRole(role) } }), + }); + + // THEN + Template.fromStack(stateMachineStack).hasResourceProperties('AWS::StepFunctions::StateMachine', { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{"StartAt":"fakeTask","States":{"fakeTask":{"End":true,"Type":"Task","Credentials":{"RoleArn":"arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::987654321:role/example-role"},"Resource":"my-resource","Parameters":{"MyParameter":"myParameter"}}}}', + ], + ], + }, + }); + + Template.fromStack(stateMachineStack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::987654321:role/example-role', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyStateMachineRoleDefaultPolicyE468EB18', + Roles: [ + { + Ref: 'MyStateMachineRoleD59FFEBC', + }, + ], + }); + }); + + test('Instantiate a State Machine with a task assuming a literal roleArn (same-account)', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const role = iam.Role.fromRoleName(stack, 'Role', 'example-role'); + new sfn.StateMachine(stack, 'MyStateMachine', { + definition: new FakeTask(stack, 'fakeTask', { credentials: { role: sfn.TaskRole.fromRole(role) } }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::StepFunctions::StateMachine', { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{"StartAt":"fakeTask","States":{"fakeTask":{"End":true,"Type":"Task","Credentials":{"RoleArn":"arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/example-role"},"Resource":"my-resource","Parameters":{"MyParameter":"myParameter"}}}}', + ], + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/example-role', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyStateMachineRoleDefaultPolicyE468EB18', + Roles: [ + { + Ref: 'MyStateMachineRoleD59FFEBC', + }, + ], + }); + }); + + test('Instantiate a State Machine with a task assuming a JSONPath roleArn', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new sfn.StateMachine(stack, 'MyStateMachine', { + definition: new FakeTask(stack, 'fakeTask', { credentials: { role: sfn.TaskRole.fromRoleArnJsonPath('$.RoleArn') } }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::StepFunctions::StateMachine', { + DefinitionString: '{"StartAt":"fakeTask","States":{"fakeTask":{"End":true,"Type":"Task","Credentials":{"RoleArn.$":"$.RoleArn"},"Resource":"my-resource","Parameters":{"MyParameter":"myParameter"}}}}', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyStateMachineRoleDefaultPolicyE468EB18', + Roles: [ + { + Ref: 'MyStateMachineRoleD59FFEBC', + }, + ], + }); + }); + describe('StateMachine.fromStateMachineArn()', () => { let stack: cdk.Stack; @@ -289,10 +449,10 @@ describe('State Machine', () => { }); describe('for a state machine in a different account and region', () => { - let mach: stepfunctions.IStateMachine; + let mach: sfn.IStateMachine; beforeEach(() => { - mach = stepfunctions.StateMachine.fromStateMachineArn( + mach = sfn.StateMachine.fromStateMachineArn( stack, 'iMach', 'arn:aws:states:machine-region:222222222222:stateMachine:machine-name', @@ -320,10 +480,10 @@ describe('State Machine', () => { }); describe('for a state machine in the same account and region', () => { - let mach: stepfunctions.IStateMachine; + let mach: sfn.IStateMachine; beforeEach(() => { - mach = stepfunctions.StateMachine.fromStateMachineName( + mach = sfn.StateMachine.fromStateMachineName( stack, 'iMach', 'machine-name', diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts index a616ca0061e11..0d6e9e7977dc9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts @@ -1,8 +1,9 @@ import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import * as constructs from 'constructs'; import * as sfn from '../lib'; +import { FakeTask } from './private/fake-task'; +import { renderGraph } from './private/render-util'; describe('Task base', () => { let stack: cdk.Stack; @@ -27,7 +28,7 @@ describe('Task base', () => { }); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-exciting-task', States: { 'my-exciting-task': { @@ -43,6 +44,65 @@ describe('Task base', () => { }); }); + test('instantiate a concrete implementation with credentials of a specified role', () => { + // WHEN + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/example-role'); + task = new FakeTask(stack, 'my-exciting-task', { + comment: 'my exciting task', + heartbeat: cdk.Duration.seconds(10), + timeout: cdk.Duration.minutes(10), + credentials: { + role: sfn.TaskRole.fromRole(role), + }, + }); + + // THEN + expect(renderGraph(task)).toEqual({ + StartAt: 'my-exciting-task', + States: { + 'my-exciting-task': { + End: true, + Type: 'Task', + Comment: 'my exciting task', + TimeoutSeconds: 600, + HeartbeatSeconds: 10, + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + Credentials: { RoleArn: 'arn:aws:iam::123456789012:role/example-role' }, + }, + }, + }); + }); + + test('instantiate a concrete implementation with credentials of json expression roleArn', () => { + // WHEN + task = new FakeTask(stack, 'my-exciting-task', { + comment: 'my exciting task', + heartbeat: cdk.Duration.seconds(10), + timeout: cdk.Duration.minutes(10), + credentials: { + role: sfn.TaskRole.fromRoleArnJsonPath('$.Input.RoleArn'), + }, + }); + + // THEN + expect(renderGraph(task)).toEqual({ + StartAt: 'my-exciting-task', + States: { + 'my-exciting-task': { + End: true, + Type: 'Task', + Comment: 'my exciting task', + TimeoutSeconds: 600, + HeartbeatSeconds: 10, + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + Credentials: { 'RoleArn.$': '$.Input.RoleArn' }, + }, + }, + }); + }); + test('instantiate a concrete implementation with resultSelector', () => { // WHEN task = new FakeTask(stack, 'my-exciting-task', { @@ -53,7 +113,7 @@ describe('Task base', () => { }); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-exciting-task', States: { 'my-exciting-task': { @@ -81,7 +141,7 @@ describe('Task base', () => { task.addCatch(failure); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-task', States: { 'my-task': { @@ -116,7 +176,7 @@ describe('Task base', () => { .addCatch(otherFailure, { errors: ['OtherError'] }); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-task', States: { 'all': { @@ -172,7 +232,7 @@ describe('Task base', () => { .addRetry(); // adds default retry // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-task', States: { 'my-task': { @@ -202,7 +262,7 @@ describe('Task base', () => { .addRetry({ errors: ['OtherError'] }); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-task', States: { 'my-task': { @@ -237,7 +297,7 @@ describe('Task base', () => { task.next(new sfn.Pass(stack, 'passState')); // THEN - expect(render(task)).toEqual({ + expect(renderGraph(task)).toEqual({ StartAt: 'my-task', States: { 'my-task': { @@ -399,35 +459,3 @@ function verifyMetric(metric: Metric, metricName: string, statistic: string) { statistic, })); } - -function render(sm: sfn.IChainable) { - return new cdk.Stack().resolve( - new sfn.StateGraph(sm.startState, 'Test Graph').toGraphJson(), - ); -} - -interface FakeTaskProps extends sfn.TaskStateBaseProps { - readonly metrics?: sfn.TaskMetricsConfig; -} - -class FakeTask extends sfn.TaskStateBase { - protected readonly taskMetrics?: sfn.TaskMetricsConfig; - protected readonly taskPolicies?: iam.PolicyStatement[]; - - constructor(scope: constructs.Construct, id: string, props: FakeTaskProps = {}) { - super(scope, id, props); - this.taskMetrics = props.metrics; - } - - /** - * @internal - */ - protected _renderTask(): any { - return { - Resource: 'my-resource', - Parameters: sfn.FieldUtils.renderObject({ - MyParameter: 'myParameter', - }), - }; - } -} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task-credentials.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task-credentials.test.ts new file mode 100644 index 0000000000000..bffa9eb177454 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/task-credentials.test.ts @@ -0,0 +1,34 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; + +describe('TaskRole', () => { + let stack: cdk.Stack; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + describe('fromRole()', () => { + test('returns expected roleArn and resource', () => { + const iamRole = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/example-role'); + const role = sfn.TaskRole.fromRole(iamRole); + + expect(stack.resolve(role.roleArn)).toEqual('arn:aws:iam::123456789012:role/example-role'); + expect(role.resource).toEqual('arn:aws:iam::123456789012:role/example-role'); + }); + }); + + describe('fromRoleArnJsonPath()', () => { + test('returns expected roleArn and resource', () => { + const role = sfn.TaskRole.fromRoleArnJsonPath('$.RoleArn'); + + expect(stack.resolve(role.roleArn)).toEqual('$.RoleArn'); + expect(role.resource).toEqual( '*'); + }); + + test('returns expected roleArn and resource', () => { + expect(() => sfn.TaskRole.fromRoleArnJsonPath('RoleArn')).toThrow(); + }); + }); +});