Skip to content

Commit aa27e6b

Browse files
committed
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
1 parent 76bdccb commit aa27e6b

File tree

4 files changed

+161
-81
lines changed

4 files changed

+161
-81
lines changed

packages/@aws-cdk/aws-dynamodb/lib/table.ts

+75-71
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ export interface ITable extends IResource {
212212
*/
213213
readonly tableName: string;
214214

215+
/**
216+
* ARN of the table's stream, if there is one.
217+
*
218+
* @attribute
219+
*/
220+
readonly tableStreamArn?: string;
221+
215222
/**
216223
* Permits an IAM principal all data read operations from this table:
217224
* BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan.
@@ -305,17 +312,24 @@ export interface TableAttributes {
305312
* The ARN of the dynamodb table.
306313
* One of this, or {@link tabeName}, is required.
307314
*
308-
* @default no table arn
315+
* @default - no table arn
309316
*/
310317
readonly tableArn?: string;
311318

312319
/**
313320
* The table name of the dynamodb table.
314321
* One of this, or {@link tabeArn}, is required.
315322
*
316-
* @default no table name
323+
* @default - no table name
317324
*/
318325
readonly tableName?: string;
326+
327+
/**
328+
* The ARN of the table's stream.
329+
*
330+
* @default - no table stream
331+
*/
332+
readonly tableStreamArn?: string;
319333
}
320334

321335
abstract class TableBase extends Resource implements ITable {
@@ -329,6 +343,11 @@ abstract class TableBase extends Resource implements ITable {
329343
*/
330344
public abstract readonly tableName: string;
331345

346+
/**
347+
* @attribute
348+
*/
349+
public abstract readonly tableStreamArn?: string;
350+
332351
/**
333352
* Adds an IAM policy statement associated with this table to an IAM
334353
* principal's policy.
@@ -347,6 +366,25 @@ abstract class TableBase extends Resource implements ITable {
347366
});
348367
}
349368

369+
/**
370+
* Adds an IAM policy statement associated with this table's stream to an
371+
* IAM principal's policy.
372+
* @param grantee The principal (no-op if undefined)
373+
* @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...)
374+
*/
375+
public grantStream(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
376+
if (!this.tableStreamArn) {
377+
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
378+
}
379+
380+
return iam.Grant.addToPrincipal({
381+
grantee,
382+
actions,
383+
resourceArns: [this.tableStreamArn],
384+
scope: this,
385+
});
386+
}
387+
350388
/**
351389
* Permits an IAM principal all data read operations from this table:
352390
* BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan.
@@ -359,17 +397,31 @@ abstract class TableBase extends Resource implements ITable {
359397
/**
360398
* Permits an IAM Principal to list streams attached to current dynamodb table.
361399
*
362-
* @param _grantee The principal (no-op if undefined)
400+
* @param grantee The principal (no-op if undefined)
363401
*/
364-
public abstract grantTableListStreams(_grantee: iam.IGrantable): iam.Grant;
402+
public grantTableListStreams(grantee: iam.IGrantable): iam.Grant {
403+
if (!this.tableStreamArn) {
404+
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
405+
}
406+
return iam.Grant.addToPrincipal({
407+
grantee,
408+
actions: ['dynamodb:ListStreams'],
409+
resourceArns: [
410+
Lazy.stringValue({ produce: () => `${this.tableArn}/stream/*` })
411+
],
412+
});
413+
}
365414

366415
/**
367416
* Permits an IAM principal all stream data read operations for this
368417
* table's stream:
369418
* DescribeStream, GetRecords, GetShardIterator, ListStreams.
370419
* @param grantee The principal to grant access to
371420
*/
372-
public abstract grantStreamRead(grantee: iam.IGrantable): iam.Grant;
421+
public grantStreamRead(grantee: iam.IGrantable): iam.Grant {
422+
this.grantTableListStreams(grantee);
423+
return this.grantStream(grantee, ...READ_STREAM_DATA_ACTIONS);
424+
}
373425

374426
/**
375427
* Permits an IAM principal all data write operations to this table:
@@ -521,47 +573,41 @@ export class Table extends TableBase {
521573

522574
public readonly tableName: string;
523575
public readonly tableArn: string;
576+
public readonly tableStreamArn?: string;
524577

525-
constructor(_scope: Construct, _id: string, _tableArn: string, _tableName: string) {
526-
super(_scope, _id);
578+
constructor(_tableArn: string, tableName: string, tableStreamArn?: string) {
579+
super(scope, id);
527580
this.tableArn = _tableArn;
528-
this.tableName = _tableName;
581+
this.tableName = tableName;
582+
this.tableStreamArn = tableStreamArn;
529583
}
530584

531585
protected get hasIndex(): boolean {
532586
return false;
533587
}
534-
535-
public grantTableListStreams(_grantee: iam.IGrantable): iam.Grant {
536-
throw new Error("Method not implemented.");
537-
}
538-
539-
public grantStreamRead(_grantee: iam.IGrantable): iam.Grant {
540-
throw new Error("Method not implemented.");
541-
}
542588
}
543589

544-
let tableName: string;
545-
let tableArn: string;
590+
let name: string;
591+
let arn: string;
546592
const stack = Stack.of(scope);
547593
if (!attrs.tableName) {
548594
if (!attrs.tableArn) { throw new Error('One of tableName or tableArn is required!'); }
549595

550-
tableArn = attrs.tableArn;
596+
arn = attrs.tableArn;
551597
const maybeTableName = stack.parseArn(attrs.tableArn).resourceName;
552598
if (!maybeTableName) { throw new Error('ARN for DynamoDB table must be in the form: ...'); }
553-
tableName = maybeTableName;
599+
name = maybeTableName;
554600
} else {
555601
if (attrs.tableArn) { throw new Error("Only one of tableArn or tableName can be provided"); }
556-
tableName = attrs.tableName;
557-
tableArn = stack.formatArn({
602+
name = attrs.tableName;
603+
arn = stack.formatArn({
558604
service: 'dynamodb',
559605
resource: 'table',
560606
resourceName: attrs.tableName,
561607
});
562608
}
563609

564-
return new Import(scope, id, tableArn, tableName);
610+
return new Import(arn, name, attrs.tableStreamArn);
565611
}
566612

567613
/**
@@ -664,54 +710,6 @@ export class Table extends TableBase {
664710
}
665711
}
666712

667-
/**
668-
* Adds an IAM policy statement associated with this table's stream to an
669-
* IAM principal's policy.
670-
* @param grantee The principal (no-op if undefined)
671-
* @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...)
672-
*/
673-
public grantStream(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
674-
if (!this.tableStreamArn) {
675-
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
676-
}
677-
678-
return iam.Grant.addToPrincipal({
679-
grantee,
680-
actions,
681-
resourceArns: [this.tableStreamArn],
682-
scope: this,
683-
});
684-
}
685-
686-
/**
687-
* Permits an IAM Principal to list streams attached to current dynamodb table.
688-
*
689-
* @param grantee The principal (no-op if undefined)
690-
*/
691-
public grantTableListStreams(grantee: iam.IGrantable): iam.Grant {
692-
if (!this.tableStreamArn) {
693-
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
694-
}
695-
return iam.Grant.addToPrincipal({
696-
grantee,
697-
actions: ['dynamodb:ListStreams'],
698-
resourceArns: [
699-
Lazy.stringValue({ produce: () => `${this.tableArn}/stream/*` })
700-
],
701-
});
702-
}
703-
704-
/**
705-
* Permits an IAM principal all stream data read operations for this
706-
* table's stream:
707-
* DescribeStream, GetRecords, GetShardIterator, ListStreams.
708-
* @param grantee The principal to grant access to
709-
*/
710-
public grantStreamRead(grantee: iam.IGrantable): iam.Grant {
711-
this.grantTableListStreams(grantee);
712-
return this.grantStream(grantee, ...READ_STREAM_DATA_ACTIONS);
713-
}
714-
715713
/**
716714
* Add a global secondary index of table.
717715
*
@@ -1088,8 +1086,11 @@ export class Table extends TableBase {
10881086
}
10891087

10901088
export enum AttributeType {
1089+
/** Up to 400KiB of binary data (which must be encoded as base64 before sending to DynamoDB) */
10911090
BINARY = 'B',
1091+
/** Numeric values made of up to 38 digits (positive, negative or zero) */
10921092
NUMBER = 'N',
1093+
/** Up to 400KiB of UTF-8 encoded text */
10931094
STRING = 'S',
10941095
}
10951096

@@ -1108,8 +1109,11 @@ export enum BillingMode {
11081109
}
11091110

11101111
export enum ProjectionType {
1112+
/** Only the index and primary keys are projected into the index. */
11111113
KEYS_ONLY = 'KEYS_ONLY',
1114+
/** Only the specified table attributes are projected into the index. The list of projected attributes is in `nonKeyAttributes`. */
11121115
INCLUDE = 'INCLUDE',
1116+
/** All of the table attributes are projected into the index. */
11131117
ALL = 'ALL'
11141118
}
11151119

packages/@aws-cdk/aws-dynamodb/package.json

+1-7
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@
9999
"awslint": {
100100
"exclude": [
101101
"docs-public-apis:@aws-cdk/aws-dynamodb.TableProps",
102-
"docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.ALL",
103102
"docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableName",
104103
"docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableStreamArn",
105104
"docs-public-apis:@aws-cdk/aws-dynamodb.Attribute",
@@ -109,12 +108,7 @@
109108
"docs-public-apis:@aws-cdk/aws-dynamodb.TableOptions",
110109
"docs-public-apis:@aws-cdk/aws-dynamodb.Table.tableArn",
111110
"docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType",
112-
"docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.BINARY",
113-
"docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.NUMBER",
114-
"docs-public-apis:@aws-cdk/aws-dynamodb.AttributeType.STRING",
115-
"docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType",
116-
"docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.KEYS_ONLY",
117-
"docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType.INCLUDE"
111+
"docs-public-apis:@aws-cdk/aws-dynamodb.ProjectionType"
118112
]
119113
}
120114
}

packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts

+84-2
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,7 @@ export = {
14611461

14621462
test.done();
14631463
},
1464-
'static import(ref) allows importing an external/existing table from arn'(test: Test) {
1464+
'static fromTableArn(arn) allows importing an external/existing table from arn'(test: Test) {
14651465
const stack = new Stack();
14661466

14671467
const tableArn = 'arn:aws:dynamodb:us-east-1:11111111:table/MyTable';
@@ -1502,7 +1502,7 @@ export = {
15021502
test.deepEqual(stack.resolve(table.tableName), 'MyTable');
15031503
test.done();
15041504
},
1505-
'static import(ref) allows importing an external/existing table from table name'(test: Test) {
1505+
'static fromTableName(name) allows importing an external/existing table from table name'(test: Test) {
15061506
const stack = new Stack();
15071507

15081508
const tableName = 'MyTable';
@@ -1568,6 +1568,88 @@ export = {
15681568
test.deepEqual(stack.resolve(table.tableName), tableName);
15691569
test.done();
15701570
},
1571+
'stream permissions on imported tables': {
1572+
'throw if no tableStreamArn is specified'(test: Test) {
1573+
const stack = new Stack();
1574+
1575+
const tableName = 'MyTable';
1576+
const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName });
1577+
1578+
const role = new iam.Role(stack, 'NewRole', {
1579+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
1580+
});
1581+
1582+
test.throws(() => table.grantTableListStreams(role), /DynamoDB Streams must be enabled on the table/);
1583+
test.throws(() => table.grantStreamRead(role), /DynamoDB Streams must be enabled on the table/);
1584+
1585+
test.done();
1586+
},
1587+
1588+
'creates the correct list streams grant'(test: Test) {
1589+
const stack = new Stack();
1590+
1591+
const tableName = 'MyTable';
1592+
const tableStreamArn = 'arn:foo:bar:baz:TrustMeThisIsATableStream';
1593+
const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName, tableStreamArn });
1594+
1595+
const role = new iam.Role(stack, 'NewRole', {
1596+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
1597+
});
1598+
1599+
test.notEqual(table.grantTableListStreams(role), null);
1600+
1601+
expect(stack).to(haveResource('AWS::IAM::Policy', {
1602+
PolicyDocument: {
1603+
Statement: [
1604+
{
1605+
Action: "dynamodb:ListStreams",
1606+
Effect: 'Allow',
1607+
Resource: stack.resolve(`${table.tableArn}/stream/*`),
1608+
},
1609+
],
1610+
Version: '2012-10-17'
1611+
},
1612+
Roles: [stack.resolve(role.roleName)]
1613+
}));
1614+
1615+
test.done();
1616+
},
1617+
1618+
'creates the correct stream read grant'(test: Test) {
1619+
const stack = new Stack();
1620+
1621+
const tableName = 'MyTable';
1622+
const tableStreamArn = 'arn:foo:bar:baz:TrustMeThisIsATableStream';
1623+
const table = Table.fromTableAttributes(stack, 'ImportedTable', { tableName, tableStreamArn });
1624+
1625+
const role = new iam.Role(stack, 'NewRole', {
1626+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
1627+
});
1628+
1629+
test.notEqual(table.grantStreamRead(role), null);
1630+
1631+
expect(stack).to(haveResource('AWS::IAM::Policy', {
1632+
PolicyDocument: {
1633+
Statement: [
1634+
{
1635+
Action: "dynamodb:ListStreams",
1636+
Effect: 'Allow',
1637+
Resource: stack.resolve(`${table.tableArn}/stream/*`),
1638+
},
1639+
{
1640+
Action: ['dynamodb:DescribeStream', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator'],
1641+
Effect: 'Allow',
1642+
Resource: tableStreamArn,
1643+
}
1644+
],
1645+
Version: '2012-10-17'
1646+
},
1647+
Roles: [stack.resolve(role.roleName)]
1648+
}));
1649+
1650+
test.done();
1651+
},
1652+
}
15711653
},
15721654

15731655
'global': {

packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface DynamoEventSourceProps extends StreamEventSourceProps {
1111
export class DynamoEventSource extends StreamEventSource {
1212
private _eventSourceMappingId?: string = undefined;
1313

14-
constructor(private readonly table: dynamodb.Table, props: DynamoEventSourceProps) {
14+
constructor(private readonly table: dynamodb.ITable, props: DynamoEventSourceProps) {
1515
super(props);
1616

1717
if (this.props.batchSize !== undefined && (this.props.batchSize < 1 || this.props.batchSize > 1000)) {

0 commit comments

Comments
 (0)