From aa27e6b531adc6eddac8d5a9a64ef73751639274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Mon, 9 Mar 2020 14:45:42 +0100 Subject: [PATCH] feat(dynamodb): expose stream features on ITable In order to make it possible to use the `DynamoEventSource` feature from `@aws-cdk/aws-lambda-event-sources` with imported tables (`ITable`s obtained from `Table.fromTableAttributes`), the `tableStreamArn` property must be visible on the `ITable` interface, and accepted as part of the `TableAttributes` struct. The necessary `grant` methods that target the table stream were also modified so that they can be used on any `ITable` that was built with a `tableStreamArn`. As a bonus, added documentation text for a couple of previously undocumented enum constants. Fixes #6344 --- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 146 +++++++++--------- packages/@aws-cdk/aws-dynamodb/package.json | 8 +- .../aws-dynamodb/test/test.dynamodb.ts | 86 ++++++++++- .../aws-lambda-event-sources/lib/dynamodb.ts | 2 +- 4 files changed, 161 insertions(+), 81 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index bfe9d69b5f85b..722faed5f3eb9 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -212,6 +212,13 @@ export interface ITable extends IResource { */ readonly tableName: string; + /** + * ARN of the table's stream, if there is one. + * + * @attribute + */ + readonly tableStreamArn?: string; + /** * Permits an IAM principal all data read operations from this table: * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. @@ -305,7 +312,7 @@ export interface TableAttributes { * The ARN of the dynamodb table. * One of this, or {@link tabeName}, is required. * - * @default no table arn + * @default - no table arn */ readonly tableArn?: string; @@ -313,9 +320,16 @@ export interface TableAttributes { * The table name of the dynamodb table. * One of this, or {@link tabeArn}, is required. * - * @default no table name + * @default - no table name */ readonly tableName?: string; + + /** + * The ARN of the table's stream. + * + * @default - no table stream + */ + readonly tableStreamArn?: string; } abstract class TableBase extends Resource implements ITable { @@ -329,6 +343,11 @@ abstract class TableBase extends Resource implements ITable { */ public abstract readonly tableName: string; + /** + * @attribute + */ + public abstract readonly tableStreamArn?: string; + /** * Adds an IAM policy statement associated with this table to an IAM * principal's policy. @@ -347,6 +366,25 @@ abstract class TableBase extends Resource implements ITable { }); } + /** + * Adds an IAM policy statement associated with this table's stream to an + * IAM principal's policy. + * @param grantee The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...) + */ + public grantStream(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + if (!this.tableStreamArn) { + throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`); + } + + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.tableStreamArn], + scope: this, + }); + } + /** * Permits an IAM principal all data read operations from this table: * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. @@ -359,9 +397,20 @@ abstract class TableBase extends Resource implements ITable { /** * Permits an IAM Principal to list streams attached to current dynamodb table. * - * @param _grantee The principal (no-op if undefined) + * @param grantee The principal (no-op if undefined) */ - public abstract grantTableListStreams(_grantee: iam.IGrantable): iam.Grant; + public grantTableListStreams(grantee: iam.IGrantable): iam.Grant { + if (!this.tableStreamArn) { + throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`); + } + return iam.Grant.addToPrincipal({ + grantee, + actions: ['dynamodb:ListStreams'], + resourceArns: [ + Lazy.stringValue({ produce: () => `${this.tableArn}/stream/*` }) + ], + }); + } /** * Permits an IAM principal all stream data read operations for this @@ -369,7 +418,10 @@ abstract class TableBase extends Resource implements ITable { * DescribeStream, GetRecords, GetShardIterator, ListStreams. * @param grantee The principal to grant access to */ - public abstract grantStreamRead(grantee: iam.IGrantable): iam.Grant; + public grantStreamRead(grantee: iam.IGrantable): iam.Grant { + this.grantTableListStreams(grantee); + return this.grantStream(grantee, ...READ_STREAM_DATA_ACTIONS); + } /** * Permits an IAM principal all data write operations to this table: @@ -521,47 +573,41 @@ export class Table extends TableBase { public readonly tableName: string; public readonly tableArn: string; + public readonly tableStreamArn?: string; - constructor(_scope: Construct, _id: string, _tableArn: string, _tableName: string) { - super(_scope, _id); + constructor(_tableArn: string, tableName: string, tableStreamArn?: string) { + super(scope, id); this.tableArn = _tableArn; - this.tableName = _tableName; + this.tableName = tableName; + this.tableStreamArn = tableStreamArn; } protected get hasIndex(): boolean { return false; } - - public grantTableListStreams(_grantee: iam.IGrantable): iam.Grant { - throw new Error("Method not implemented."); - } - - public grantStreamRead(_grantee: iam.IGrantable): iam.Grant { - throw new Error("Method not implemented."); - } } - let tableName: string; - let tableArn: string; + let name: string; + let arn: string; const stack = Stack.of(scope); if (!attrs.tableName) { if (!attrs.tableArn) { throw new Error('One of tableName or tableArn is required!'); } - tableArn = attrs.tableArn; + arn = attrs.tableArn; const maybeTableName = stack.parseArn(attrs.tableArn).resourceName; if (!maybeTableName) { throw new Error('ARN for DynamoDB table must be in the form: ...'); } - tableName = maybeTableName; + name = maybeTableName; } else { if (attrs.tableArn) { throw new Error("Only one of tableArn or tableName can be provided"); } - tableName = attrs.tableName; - tableArn = stack.formatArn({ + name = attrs.tableName; + arn = stack.formatArn({ service: 'dynamodb', resource: 'table', resourceName: attrs.tableName, }); } - return new Import(scope, id, tableArn, tableName); + return new Import(arn, name, attrs.tableStreamArn); } /** @@ -664,54 +710,6 @@ export class Table extends TableBase { } } - /** - * Adds an IAM policy statement associated with this table's stream to an - * IAM principal's policy. - * @param grantee The principal (no-op if undefined) - * @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...) - */ - public grantStream(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - if (!this.tableStreamArn) { - throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`); - } - - return iam.Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [this.tableStreamArn], - scope: this, - }); - } - - /** - * Permits an IAM Principal to list streams attached to current dynamodb table. - * - * @param grantee The principal (no-op if undefined) - */ - public grantTableListStreams(grantee: iam.IGrantable): iam.Grant { - if (!this.tableStreamArn) { - throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`); - } - return iam.Grant.addToPrincipal({ - grantee, - actions: ['dynamodb:ListStreams'], - resourceArns: [ - Lazy.stringValue({ produce: () => `${this.tableArn}/stream/*` }) - ], - }); - } - - /** - * Permits an IAM principal all stream data read operations for this - * table's stream: - * DescribeStream, GetRecords, GetShardIterator, ListStreams. - * @param grantee The principal to grant access to - */ - public grantStreamRead(grantee: iam.IGrantable): iam.Grant { - this.grantTableListStreams(grantee); - return this.grantStream(grantee, ...READ_STREAM_DATA_ACTIONS); - } - /** * Add a global secondary index of table. * @@ -1088,8 +1086,11 @@ export class Table extends TableBase { } export enum AttributeType { + /** Up to 400KiB of binary data (which must be encoded as base64 before sending to DynamoDB) */ BINARY = 'B', + /** Numeric values made of up to 38 digits (positive, negative or zero) */ NUMBER = 'N', + /** Up to 400KiB of UTF-8 encoded text */ STRING = 'S', } @@ -1108,8 +1109,11 @@ export enum BillingMode { } export enum ProjectionType { + /** Only the index and primary keys are projected into the index. */ KEYS_ONLY = 'KEYS_ONLY', + /** Only the specified table attributes are projected into the index. The list of projected attributes is in `nonKeyAttributes`. */ INCLUDE = 'INCLUDE', + /** All of the table attributes are projected into the index. */ ALL = 'ALL' } diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index ad1e6435cd970..5b7aa1f8a659a 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -99,7 +99,6 @@ "awslint": { "exclude": [ "docs-public-apis:@aws-cdk/aws-dynamodb.TableProps", - "docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.ALL", "docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableName", "docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableStreamArn", "docs-public-apis:@aws-cdk/aws-dynamodb.Attribute", @@ -109,12 +108,7 @@ "docs-public-apis:@aws-cdk/aws-dynamodb.TableOptions", "docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableArn", "docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType", - "docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.BINARY", - "docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.NUMBER", - "docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.STRING", - "docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType", - "docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.KEYS_ONLY", - "docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.INCLUDE" + "docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType" ] } } diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index dd4fc1c30d54d..83d3709d1e9a7 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1461,7 +1461,7 @@ export = { test.done(); }, - 'static import(ref) allows importing an external/existing table from arn'(test: Test) { + 'static fromTableArn(arn) allows importing an external/existing table from arn'(test: Test) { const stack = new Stack(); const tableArn = 'arn:aws:dynamodb:us-east-1:11111111:table/MyTable'; @@ -1502,7 +1502,7 @@ export = { test.deepEqual(stack.resolve(table.tableName), 'MyTable'); test.done(); }, - 'static import(ref) allows importing an external/existing table from table name'(test: Test) { + 'static fromTableName(name) allows importing an external/existing table from table name'(test: Test) { const stack = new Stack(); const tableName = 'MyTable'; @@ -1568,6 +1568,88 @@ export = { test.deepEqual(stack.resolve(table.tableName), tableName); test.done(); }, + 'stream permissions on imported tables': { + 'throw if no tableStreamArn is specified'(test: Test) { + const stack = new Stack(); + + const tableName = 'MyTable'; + const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName }); + + const role = new iam.Role(stack, 'NewRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + test.throws(() => table.grantTableListStreams(role), /DynamoDB Streams must be enabled on the table/); + test.throws(() => table.grantStreamRead(role), /DynamoDB Streams must be enabled on the table/); + + test.done(); + }, + + 'creates the correct list streams grant'(test: Test) { + const stack = new Stack(); + + const tableName = 'MyTable'; + const tableStreamArn = 'arn:foo:bar:baz:TrustMeThisIsATableStream'; + const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName, tableStreamArn }); + + const role = new iam.Role(stack, 'NewRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + test.notEqual(table.grantTableListStreams(role), null); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:ListStreams", + Effect: 'Allow', + Resource: stack.resolve(`${table.tableArn}/stream/*`), + }, + ], + Version: '2012-10-17' + }, + Roles: [stack.resolve(role.roleName)] + })); + + test.done(); + }, + + 'creates the correct stream read grant'(test: Test) { + const stack = new Stack(); + + const tableName = 'MyTable'; + const tableStreamArn = 'arn:foo:bar:baz:TrustMeThisIsATableStream'; + const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName, tableStreamArn }); + + const role = new iam.Role(stack, 'NewRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + test.notEqual(table.grantStreamRead(role), null); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:ListStreams", + Effect: 'Allow', + Resource: stack.resolve(`${table.tableArn}/stream/*`), + }, + { + Action: ['dynamodb:DescribeStream', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator'], + Effect: 'Allow', + Resource: tableStreamArn, + } + ], + Version: '2012-10-17' + }, + Roles: [stack.resolve(role.roleName)] + })); + + test.done(); + }, + } }, 'global': { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts index 9e520a6edd5f7..d5bad30634d2e 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts @@ -11,7 +11,7 @@ export interface DynamoEventSourceProps extends StreamEventSourceProps { export class DynamoEventSource extends StreamEventSource { private _eventSourceMappingId?: string = undefined; - constructor(private readonly table: dynamodb.Table, props: DynamoEventSourceProps) { + constructor(private readonly table: dynamodb.ITable, props: DynamoEventSourceProps) { super(props); if (this.props.batchSize !== undefined && (this.props.batchSize < 1 || this.props.batchSize > 1000)) {