From 037fc09c856cd3376309bd77b486780a28efe9a0 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Wed, 17 Jun 2020 18:47:58 -0400 Subject: [PATCH] updated grant APIs --- packages/@aws-cdk/aws-stepfunctions/README.md | 97 +++++++++++-------- .../aws-stepfunctions/lib/activity.ts | 15 +++ .../aws-stepfunctions/lib/state-machine.ts | 45 +++++---- .../aws-stepfunctions/test/activity.test.ts | 30 ++++++ .../test/integ.custom-state.expected.json | 4 +- .../test/integ.state-machine.ts | 2 +- .../test/state-machine-resources.test.ts | 83 ++++++++-------- 7 files changed, 171 insertions(+), 105 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index d7348e8b96506..1cf3623c02295 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -458,6 +458,22 @@ const activity = new stepfunctions.Activity(this, 'Activity'); new cdk.CfnOutput(this, 'ActivityArn', { value: activity.activityArn }); ``` +### Activity-Level Permissions + +Granting IAM permissions to an activity can be achieved by calling the `grant(principal, actions)` API: + +```ts +const activity = new stepfunctions.Activity(this, 'Activity'); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +activity.grant(role, ['states:SendTaskSuccess']); +``` + +This will grant the IAM principal the specified actions onto the activity. + ## Metrics `Task` object expose various metrics on the execution of that particular task. For example, @@ -514,96 +530,99 @@ IAM roles, users, or groups which need to be able to work with a State Machine s Any object that implements the `IGrantable` interface (has an associated principal) can be granted permissions by calling: -- `stateMachine.grantStartExecution(principal)` - grants the principal the ability to start an execution +- `stateMachine.grantStartExecution(principal)` - grants the principal the ability to execute the state machine - `stateMachine.grantRead(principal)` - grants the principal read access -- `stateMachine.grant(principal, actions, resourceArn)` - grants the principal the specific IAM action specified +- `stateMachine.grantTaskResponse(principal)` - grants the principal the ability to send task tokens to the state machine +- `stateMachine.grantExecution(principal, actions)` - grants the principal execution-level permissions for the IAM actions specified +- `stateMachine.grant(principal, actions)` - grants the principal state-machine-level permissions for the IAM actions specified -### Read Permissions +### Start Execution Permission -Grant `read` access to a state machine by calling the `grantRead()` API. +Grant permission to start an execution of a state machine by calling the `grantStartExecution()` API. ```ts const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); -const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { definition, }); -//give role read access to state machine -stateMachine.grantRead(role); +// Give role permission to start execution of state machine +stateMachine.grantStartExecution(role); ``` -The following read permissions are provided to a service principal by the `grantRead()` API: +The following permission is provided to a service principal by the `grantStartExecution()` API: -- `states:ListExecutions` - to state machine -- `states:ListStateMachines` - to state machine -- `states:DescribeExecution` - to executions -- `states:DescribeStateMachineForExecution` - to executions -- `states:GetExecutionHistory` - to executions -- `states:ListActivities` - to `*` -- `states:DescribeStateMachine` - to `*` -- `states:DescribeActivity` - to `*` +- `states:StartExecution` - to state machine -### Start Execution Permission +### Read Permissions -Grant permission to start an execution of a state machine by calling the `grantStartExecution()` API. +Grant `read` access to a state machine by calling the `grantRead()` API. ```ts const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); -const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { definition, }); -//give role permission to start execution of state machine -stateMachine.grantStartExecution(role); +// Give role read access to state machine +stateMachine.grantRead(role); ``` -The following permission is provided to a service principal by the `grantStartExecution()` API: +The following read permissions are provided to a service principal by the `grantRead()` API: -- `states:StartExecution` - to state machine +- `states:ListExecutions` - to state machine +- `states:ListStateMachines` - to state machine +- `states:DescribeExecution` - to executions +- `states:DescribeStateMachineForExecution` - to executions +- `states:GetExecutionHistory` - to executions +- `states:ListActivities` - to `*` +- `states:DescribeStateMachine` - to `*` +- `states:DescribeActivity` - to `*` ### Task Response Permissions -Grant permission to allow task responses to a state machine by calling the `grantTaskResponse()` API. - -Task responses can be scoped down either to the state machine that holds a task that will use the callback pattern, -or an activity task with a separate `ARN`. - -Grant permission to the state machine: +Grant permission to allow task responses to a state machine by calling the `grantTaskResponse()` API: ```ts const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); -const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { definition, }); -//default arn is the state machine arn +// Give role task response permissions to the state machine stateMachine.grantTaskResponse(role); ``` -Grant permission to an activity task: +The following read permissions are provided to a service principal by the `grantRead()` API: + +- `states:SendTaskSuccess` - to state machine +- `states:SendTaskFailure` - to state machine +- `states:SendTaskHeartbeat` - to state machine + +### Execution-level Permissions + +Grant execution-level permissions to a state machine by calling the `grantExecution()` API: ```ts const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); -const activityArn = 'arn:aws:states:*:*:activity:ActivityPrefix*'; - -const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { definition, }); -//specify the activity-level ARN for the task response -stateMachine.grantTaskResponse(role, activityArn); +// Give role permission to get execution history of ALL executions for the state machine +stateMachine.grantExecution(role, ['states:GetExecutionHistory']); ``` ### Custom Permissions @@ -613,12 +632,12 @@ You can add any set of permissions to a state machine by calling the `grant()` A ```ts const user = new iam.User(stack, 'MyUser'); -const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { definition, }); //give user permission to send task success to the state machine -stateMachine.grant(user, ['states:SendTaskSuccess'], stateMachine.stateMachineArn); +stateMachine.grant(user, ['states:SendTaskSuccess']); ``` ## Future work diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 97bcbb070b06a..c7fa539f7c525 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,4 +1,5 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnActivity } from './stepfunctions.generated'; @@ -73,6 +74,20 @@ export class Activity extends Resource implements IActivity { this.activityName = this.getResourceNameAttribute(resource.attrName); } + /** + * Grant the given identity permissions on this Activity + * + * @param identity The principal + * @param actions The list of desired actions + */ + public grant(identity: iam.IGrantable, actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions, + resourceArns: [this.activityArn], + }); + } + /** * Return the given named metric for this Activity * diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index de7d5c628f5d7..b89bc736388ba 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -183,19 +183,9 @@ abstract class StateMachineBase extends Resource implements IStateMachine { } /** - * Grant the given identity task response permissions for a resource - * - * @default stateMachineArn + * Grant the given identity task response permissions on a state machine */ - public grantTaskResponse(identity: iam.IGrantable, activityArn?: string): iam.Grant { - // Validate activityArn - if (activityArn && Arn.parse(activityArn, ':').resource !== 'activity') { - throw new Error('activityArn must be a valid activity ARN'); - } - - // ActivityArn is valid if it exists - const arn = activityArn || this.stateMachineArn; - + public grantTaskResponse(identity: iam.IGrantable): iam.Grant { return iam.Grant.addToPrincipal({ grantee: identity, actions: [ @@ -203,18 +193,29 @@ abstract class StateMachineBase extends Resource implements IStateMachine { 'states:SendTaskFailure', 'states:SendTaskHeartbeat', ], - resourceArns: [arn], + resourceArns: [this.stateMachineArn], + }); + } + + /** + * Grant the given identity permissions on all executions of the state machine + */ + public grantExecution(identity: iam.IGrantable, actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions, + resourceArns: [`${this.executionArn()}:*`], }); } /** * Grant the given identity custom permissions */ - public grant(identity: iam.IGrantable, actions: string[], resourceArn: string): iam.Grant { + public grant(identity: iam.IGrantable, actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ grantee: identity, actions, - resourceArns: [resourceArn], + resourceArns: [this.stateMachineArn], }); } @@ -434,16 +435,22 @@ export interface IStateMachine extends IResource { * Grant the given identity read permissions for this state machine * * @param identity The principal - * @param resourceArn The ARN of the resource */ - grantTaskResponse(identity: iam.IGrantable, resourceArn?: string): iam.Grant; + grantTaskResponse(identity: iam.IGrantable): iam.Grant; + + /** + * Grant the given identity permissions for all executions of a state machine + * + * @param identity The principal + * @param actions The list of desired actions + */ + grantExecution(identity: iam.IGrantable, actions: string[]): iam.Grant; /** * Grant the given identity custom permissions * * @param identity The principal * @param actions The list of desired actions - * @param resourceArn The ARN of the resource */ - grant(identity: iam.IGrantable, actions: string[], resourceArn: string): iam.Grant; + grant(identity: iam.IGrantable, actions: string[]): iam.Grant; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts index 7c637b22a1fdf..51c8b1172d153 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts @@ -1,4 +1,6 @@ +import { arrayWith, objectLike } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as stepfunctions from '../lib'; @@ -41,4 +43,32 @@ describe('Activity', () => { statistic: 'Sum', }); }); + + test('Activity can grant permissions to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + const activity = new stepfunctions.Activity(stack, 'Activity'); + + // WHEN + activity.grant(role, ['states:SendTaskSuccess']); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: 'states:SendTaskSuccess', + Effect: 'Allow', + Resource: { + Ref: 'Activity04690B0A', + }, + })), + }, + }); + + }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json index 311e28c953615..d89acdeed9123 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json @@ -31,13 +31,13 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { - "DefinitionString": "{\"StartAt\":\"my custom task\",\"States\":{\"my custom task\":{\"Next\":\"final step\",\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}},\"ResultPath\":null},\"final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}", "RoleArn": { "Fn::GetAtt": [ "StateMachineRoleB840431D", "Arn" ] - } + }, + "DefinitionString": "{\"StartAt\":\"my custom task\",\"States\":{\"my custom task\":{\"Next\":\"final step\",\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}},\"ResultPath\":null},\"final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" }, "DependsOn": [ "StateMachineRoleB840431D" diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts index 8b0136ff6aa02..1c925e59f0660 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts @@ -23,6 +23,6 @@ const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { }); stateMachine.grantRead(role); -stateMachine.grant(role, ['states:SendTaskSuccess'], stateMachine.stateMachineArn); +stateMachine.grant(role, ['states:SendTaskSuccess']); app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 0f9ed818b1aa0..edf15f1db5968 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -322,10 +322,9 @@ describe('State Machine Resources', () => { ], }, }); - }), - test('Created state machine can grant task response actions to an activity', () => { + test('Created state machine can grant actions to the executions', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -333,7 +332,6 @@ describe('State Machine Resources', () => { bind: () => ({ resourceArn: 'resource' }), }, }); - const activityArn = 'arn:aws:states:*:*:activity:*'; const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { definition: task, }); @@ -342,25 +340,53 @@ describe('State Machine Resources', () => { }); // WHEN - stateMachine.grantTaskResponse(role, activityArn); + stateMachine.grantExecution(role, ['states:GetExecutionHistory']); // THEN expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Action: [ - 'states:SendTaskSuccess', - 'states:SendTaskFailure', - 'states:SendTaskHeartbeat', - ], + Action: 'states:GetExecutionHistory', Effect: 'Allow', - Resource: activityArn, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'StateMachine2E01A3A5', + }, + ], + }, + ], + }, + ':*', + ], + ], + }, }, ], }, }); - }), test('Created state machine can grant actions to a role', () => { @@ -379,7 +405,7 @@ describe('State Machine Resources', () => { }); // WHEN - stateMachine.grant(role, ['states:ListExecution'], stateMachine.stateMachineArn); + stateMachine.grant(role, ['states:ListExecution']); // THEN expect(stack).toHaveResourceLike('AWS::IAM::Policy', { @@ -527,37 +553,6 @@ describe('State Machine Resources', () => { }); }), - test('Imported state machine can task response permissions to an activity', () => { - // GIVEN - const stack = new cdk.Stack(); - const stateMachineArn = 'arn:aws:states:::my-state-machine'; - const activityArn = 'arn:aws:states:*:*:activity:*'; - const stateMachine = stepfunctions.StateMachine.fromStateMachineArn(stack, 'StateMachine', stateMachineArn); - const role = new iam.Role(stack, 'Role', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - }); - - // WHEN - stateMachine.grantTaskResponse(role, activityArn); - - // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: [ - 'states:SendTaskSuccess', - 'states:SendTaskFailure', - 'states:SendTaskHeartbeat', - ], - Effect: 'Allow', - Resource: activityArn, - }, - ], - }, - }); - }), - test('Imported state machine can grant access to a role', () => { // GIVEN const stack = new cdk.Stack(); @@ -568,7 +563,7 @@ describe('State Machine Resources', () => { }); // WHEN - stateMachine.grant(role, ['states:ListExecution'], stateMachine.stateMachineArn); + stateMachine.grant(role, ['states:ListExecution']); // THEN expect(stack).toHaveResourceLike('AWS::IAM::Policy', {