Skip to content

Commit

Permalink
feat(dynamodb): expose stream features on ITable
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RomainMuller committed Mar 9, 2020
1 parent 76bdccb commit aa27e6b
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 81 deletions.
146 changes: 75 additions & 71 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -305,17 +312,24 @@ 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;

/**
* 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 {
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -359,17 +397,31 @@ 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
* table's stream:
* 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:
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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',
}

Expand All @@ -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'
}

Expand Down
8 changes: 1 addition & 7 deletions packages/@aws-cdk/aws-dynamodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
]
}
}
86 changes: 84 additions & 2 deletions packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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': {
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down

0 comments on commit aa27e6b

Please sign in to comment.