Skip to content

Commit

Permalink
feat(stepfunctions): grant APIs for state machine construct (#8486)
Browse files Browse the repository at this point in the history
This PR closes #5933. It adds additional grant APIs to supplement `grantStartExecution()`. 

API additions to `state-machine.ts`:

- `grantRead()` method that grants read access to a role.
- `grantTaskResponse()` method that grants task response access to a role.
- `grantExecution()` method that allows user to specify what action to map onto all executions.
- `grant()` method that allows user to specify what action to map onto the state machine.

API additions to `activity.ts`:

- `grant()` method that allows user to specify what action to map onto the activity.


The idea behind these API methods is to mimic the convention of other `grant()` APIs in other modules. This is slightly more difficult with Step Functions because of the multiple resources that IAM actions can map onto. The rationale is that `grant()` in `state-machine.ts` will handle all custom permissions for the state machine, `grantExecution()` in `state-machine.ts` will handle all custom permissions for the executions, and `grant()` in `activity.ts` will handle all custom permissions for activities.

`grantRead()` and `grantTaskResponse()` are convenience APIs that were specified in the original issue for this feature.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
kaizencc committed Jun 22, 2020
1 parent a186c24 commit fe71364
Show file tree
Hide file tree
Showing 8 changed files with 819 additions and 14 deletions.
132 changes: 132 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -508,6 +524,122 @@ new stepfunctions.StateMachine(stack, 'MyStateMachine', {
});
```

## State Machine Permission Grants

IAM roles, users, or groups which need to be able to work with a State Machine should be granted IAM permissions.

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 execute the state machine
- `stateMachine.grantRead(principal)` - grants the principal read access
- `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

### Start Execution Permission

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 stepfunction.StateMachine(stack, 'StateMachine', {
definition,
});

// Give role permission to start execution of state machine
stateMachine.grantStartExecution(role);
```

The following permission is provided to a service principal by the `grantStartExecution()` API:

- `states:StartExecution` - to state machine

### Read Permissions

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 stepfunction.StateMachine(stack, 'StateMachine', {
definition,
});

// Give role read access to state machine
stateMachine.grantRead(role);
```

The following read permissions are provided to a service principal by the `grantRead()` 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 `*`

### Task Response Permissions

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 stepfunction.StateMachine(stack, 'StateMachine', {
definition,
});

// Give role task response permissions to the state machine
stateMachine.grantTaskResponse(role);
```

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 stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', {
definition,
});

// Give role permission to get execution history of ALL executions for the state machine
stateMachine.grantExecution(role, 'states:GetExecutionHistory');
```

### Custom Permissions

You can add any set of permissions to a state machine by calling the `grant()` API.

```ts
const user = new iam.User(stack, 'MyUser');

const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', {
definition,
});

//give user permission to send task success to the state machine
stateMachine.grant(user, 'states:SendTaskSuccess');
```

## Import

Any Step Functions state machine that has been created outside the stack can be imported
Expand Down
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions/lib/activity.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
*
Expand Down
116 changes: 114 additions & 2 deletions packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';
import { Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core';
import { Arn, Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core';
import { StateGraph } from './state-graph';
import { CfnStateMachine } from './stepfunctions.generated';
import { IChainable } from './types';
Expand Down Expand Up @@ -124,14 +124,14 @@ export interface StateMachineProps {
* A new or imported state machine.
*/
abstract class StateMachineBase extends Resource implements IStateMachine {

/**
* Import a state machine
*/
public static fromStateMachineArn(scope: Construct, id: string, stateMachineArn: string): IStateMachine {
class Import extends StateMachineBase {
public readonly stateMachineArn = stateMachineArn;
}

return new Import(scope, id);
}

Expand All @@ -148,6 +148,88 @@ abstract class StateMachineBase extends Resource implements IStateMachine {
resourceArns: [this.stateMachineArn],
});
}

/**
* Grant the given identity permissions to read results from state
* machine.
*/
public grantRead(identity: iam.IGrantable): iam.Grant {
iam.Grant.addToPrincipal({
grantee: identity,
actions: [
'states:ListExecutions',
'states:ListStateMachines',
],
resourceArns: [this.stateMachineArn],
});
iam.Grant.addToPrincipal({
grantee: identity,
actions: [
'states:DescribeExecution',
'states:DescribeStateMachineForExecution',
'states:GetExecutionHistory',
],
resourceArns: [`${this.executionArn()}:*`],
});
return iam.Grant.addToPrincipal({
grantee: identity,
actions: [
'states:ListActivities',
'states:DescribeStateMachine',
'states:DescribeActivity',
],
resourceArns: ['*'],
});
}

/**
* Grant the given identity task response permissions on a state machine
*/
public grantTaskResponse(identity: iam.IGrantable): iam.Grant {
return iam.Grant.addToPrincipal({
grantee: identity,
actions: [
'states:SendTaskSuccess',
'states:SendTaskFailure',
'states:SendTaskHeartbeat',
],
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[]): iam.Grant {
return iam.Grant.addToPrincipal({
grantee: identity,
actions,
resourceArns: [this.stateMachineArn],
});
}

/**
* Returns the pattern for the execution ARN's of the state machine
*/
private executionArn(): string {
return Stack.of(this).formatArn({
resource: 'execution',
service: 'states',
resourceName: Arn.parse(this.stateMachineArn, ':').resourceName,
sep: ':',
});
}
}

/**
Expand Down Expand Up @@ -341,4 +423,34 @@ export interface IStateMachine extends IResource {
* @param identity The principal
*/
grantStartExecution(identity: iam.IGrantable): iam.Grant;

/**
* Grant the given identity read permissions for this state machine
*
* @param identity The principal
*/
grantRead(identity: iam.IGrantable): iam.Grant;

/**
* Grant the given identity read permissions for this state machine
*
* @param identity The principal
*/
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
*/
grant(identity: iam.IGrantable, ...actions: string[]): iam.Grant;
}
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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',
},
})),
},
});

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit fe71364

Please sign in to comment.