Skip to content

Commit

Permalink
Stream resource policy for Table(v1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Hannigan committed Sep 21, 2024
1 parent eb2dfda commit 0fc111a
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { App, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';

export class TestStack extends Stack {

readonly table: dynamodb.Table;

constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const doc = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['dynamodb:GetRecords', 'dynamodb:DescribeStream'],
principals: [new iam.AccountRootPrincipal()],
resources: ['*'],
}),
],
});

this.table = new dynamodb.Table(this, 'TableTest1', {
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
removalPolicy: RemovalPolicy.DESTROY,
streamResourcePolicy: doc,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});

}
}

const app = new App();
const stack = new TestStack(app, 'resource-policy-stack', {});

new IntegTest(app, 'resource-policy-integ-test', {
testCases: [stack],
});
26 changes: 25 additions & 1 deletion packages/aws-cdk-lib/aws-dynamodb/TABLE_V1_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,28 @@ new dynamodb.Table(this, 'MyTable', {
});
```

If you have a global table replica, note that it does not support the addition of a resource-based policy.
If you have a global table replica, note that it does not support the addition of a resource-based policy.

Using `streamResourcePolicy` you can add a [resource policy](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/access-control-resource-based.html) to a table's stream in the form of a `PolicyDocument`:

```ts
const policy = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['dynamodb:GetRecords'],
principals: [new iam.AccountRootPrincipal()],
resources: ['*'],
}),
],
});

new dynamodb.Table(this, 'MyTable', {
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
removalPolicy: RemovalPolicy.DESTROY,
streamResourcePolicy: policy,
stream: StreamViewType.NEW_AND_OLD_IMAGES,
});
```
38 changes: 36 additions & 2 deletions packages/aws-cdk-lib/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,13 @@ export interface TableOptions extends SchemaOptions {
* @default - No resource policy statement
*/
readonly resourcePolicy?: iam.PolicyDocument;

/**
* Resource policy to assign to a DynamoDB stream.
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-globaltable-replicaspecification.html#cfn-dynamodb-globaltable-replicaspecification-replicastreamspecification
* @default - No resource policy statements are added to the stream
*/
readonly streamResourcePolicy?: iam.PolicyDocument;
}

/**
Expand Down Expand Up @@ -551,6 +558,12 @@ export abstract class TableBase extends Resource implements ITable, iam.IResourc
*/
public abstract resourcePolicy?: iam.PolicyDocument;

/**
* Resource policy to assign to table.
* @attribute
*/
public abstract streamResourcePolicy?: iam.PolicyDocument;

protected readonly regionalArns = new Array<string>();

/**
Expand Down Expand Up @@ -1047,6 +1060,7 @@ export class Table extends TableBase {
public readonly tableStreamArn?: string;
public readonly encryptionKey?: kms.IKey;
public resourcePolicy?: iam.PolicyDocument;
public streamResourcePolicy?: iam.PolicyDocument;
protected readonly hasIndex = (attrs.grantIndexPermissions ?? false) ||
(attrs.globalIndexes ?? []).length > 0 ||
(attrs.localIndexes ?? []).length > 0;
Expand Down Expand Up @@ -1092,6 +1106,13 @@ export class Table extends TableBase {
*/
public resourcePolicy?: iam.PolicyDocument;

/**
* Resource policy to assign to DynamoDB stream.
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-table-resourcepolicy.html
* @default - No resource policy statements are added to the created stream.
*/
public streamResourcePolicy?: iam.PolicyDocument;

/**
* @attribute
*/
Expand Down Expand Up @@ -1139,15 +1160,28 @@ export class Table extends TableBase {
if (props.stream && props.stream !== StreamViewType.NEW_AND_OLD_IMAGES) {
throw new Error('`stream` must be set to `NEW_AND_OLD_IMAGES` when specifying `replicationRegions`');
}
streamSpecification = { streamViewType: StreamViewType.NEW_AND_OLD_IMAGES };
streamSpecification = {
streamViewType: StreamViewType.NEW_AND_OLD_IMAGES,
resourcePolicy: props.streamResourcePolicy
? { policyDocument: props.streamResourcePolicy }
: undefined,
};

this.billingMode = props.billingMode ?? BillingMode.PAY_PER_REQUEST;
} else {
this.billingMode = props.billingMode ?? BillingMode.PROVISIONED;
if (props.stream) {
streamSpecification = { streamViewType: props.stream };
streamSpecification = {
streamViewType: props.stream,
resourcePolicy: props.streamResourcePolicy
? { policyDocument: props.streamResourcePolicy }
: undefined,
};
}
}
if (props.streamResourcePolicy && !props.stream) {
throw new Error('`stream` must be enabled when specifying `streamResourcePolicy`');
}
this.validateProvisioning(props);

this.table = new CfnTable(this, 'Resource', {
Expand Down
75 changes: 75 additions & 0 deletions packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3601,3 +3601,78 @@ test('Resource policy test', () => {
},
});
});

test('Stream resource policy test to fail as no stream specified', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'Stack');

const doc = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['dynamodb:DescribeStream', 'dynamodb:GetRecords'],
principals: [new iam.ArnPrincipal('arn:aws:iam::111122223333:user/foobar')],
resources: ['*'],
}),
],
});

expect(() => new Table(stack, 'Table', {
partitionKey: TABLE_PARTITION_KEY,
streamResourcePolicy: doc,
})).toThrow('`stream` must be enabled when specifying `streamResourcePolicy`');

});

test('Stream resource policy test', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'Stack');

const doc = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['dynamodb:DescribeStream', 'dynamodb:GetRecords'],
principals: [new iam.ArnPrincipal('arn:aws:iam::111122223333:user/foobar')],
resources: ['*'],
}),
],
});

// WHEN
const table = new Table(stack, 'Table', {
partitionKey: { name: 'id', type: AttributeType.STRING },
streamResourcePolicy: doc,
stream: StreamViewType.NEW_AND_OLD_IMAGES,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', {
KeySchema: [
{ AttributeName: 'id', KeyType: 'HASH' },
],
AttributeDefinitions: [
{ AttributeName: 'id', AttributeType: 'S' },
],
});

Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', {
'StreamSpecification': {
'ResourcePolicy': {
'PolicyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Principal': {
'AWS': 'arn:aws:iam::111122223333:user/foobar',
},
'Effect': 'Allow',
'Action': ['dynamodb:DescribeStream', 'dynamodb:GetRecords'],
'Resource': '*',
},
],
},
},
},
});
});

0 comments on commit 0fc111a

Please sign in to comment.