diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index e476c4feb59ac..4084b7feb0baf 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -19,6 +19,22 @@ const table = new dynamodb.Table(this, 'Table', { }); ``` +### Importing existing tables + +To import an existing table into your CDK application, use the `Table.fromTableAttributes` +factory method. This method accepts `TableAttributes` which describes the properties of an already +existing table: + +```ts +const table = Table.fromTableAttributes(this, 'ImportedTable', { + tableArn: 'arn:aws:dynamodb:::table/my-table', + hasIndex: true +}); + +// now you can just call methods on the table +table.grantReadWriteData(user); +``` + ### Keys When a table is defined, you must define it's schema using the `partitionKey` diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 9d5a114710be1..bd63235664d36 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1,6 +1,6 @@ import appscaling = require('@aws-cdk/aws-applicationautoscaling'); import iam = require('@aws-cdk/aws-iam'); -import { Aws, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; +import { Aws, Construct, IConstruct, IResource, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import { CfnTable } from './dynamodb.generated'; import { EnableScalingProps, IScalableTableAttribute } from './scalable-attribute-api'; import { ScalableTableAttribute } from './scalable-table-attribute'; @@ -182,10 +182,245 @@ export interface LocalSecondaryIndexProps extends SecondaryIndexProps { readonly sortKey: Attribute; } +/** + * Imported or created dynamodb table + */ +export interface ITable extends IResource { + /** + * arn of the dynamodb table + * + * @attribute + */ + readonly tableArn: string; + + /** + * table name of the dynamodb table + * + * @attribute + */ + readonly tableName: string; + + /** + * stream arn of the dynamodb table + * + * @attribute + */ + readonly tableStreamArn?: string; + + /** + * Adds an IAM policy statement associated with this table 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:PutItem", "dynamodb:GetItem", ...) + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * 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", ...) + */ + grantStream(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * Permits an IAM principal all data read operations from this table: + * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. + * @param grantee The principal to grant access to + */ + grantReadData(grantee: iam.IGrantable): iam.Grant; + + /** + * Permits an IAM Principal to list streams attached to current dynamodb table. + * + * @param grantee The principal (no-op if undefined) + */ + grantTableListStreams(grantee: iam.IGrantable): iam.Grant; + + /** + * 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 + */ + grantStreamRead(grantee: iam.IGrantable): iam.Grant; + + /** + * Permits an IAM principal all data write operations to this table: + * BatchWriteItem, PutItem, UpdateItem, DeleteItem. + * @param grantee The principal to grant access to + */ + grantWriteData(grantee: iam.IGrantable): iam.Grant; + + /** + * Permits an IAM principal to all data read/write operations to this table. + * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, + * BatchWriteItem, PutItem, UpdateItem, DeleteItem + * @param grantee The principal to grant access to + */ + grantReadWriteData(grantee: iam.IGrantable): iam.Grant; + + /** + * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. + * @param grantee The principal to grant access to + */ + grantFullAccess(grantee: iam.IGrantable): iam.Grant; +} + +/** + * Reference to a dynamodb table. + */ +export interface TableAttributes { + /** + * The ARN of the dynamodb table. + * + * @default no table arn + */ + readonly tableArn?: string; + + /** + * The table name of the dynamodb table. + * + * @default no table name + */ + readonly tableName?: string; + + /** + * The stream arn of the dynamodb. + * + * @default no table stream arn + */ + readonly tableStreamArn?: string; + + /** + * Indicate if the table has global or local secondary indexes. + */ + readonly hasIndex: boolean; +} +abstract class TableBase extends Resource implements ITable { + /** + * @attribute + */ + public abstract readonly tableArn: string; + + /** + * @attribute + */ + public abstract readonly tableName: string; + + /** + * @attribute + */ + public abstract readonly tableStreamArn: string | undefined; + + /** + * Adds an IAM policy statement associated with this table 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:PutItem", "dynamodb:GetItem", ...) + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [ + this.tableArn, + Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }) + ], + scope: this, + }); + } + + /** + * 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. + * @param grantee The principal to grant access to + */ + public grantReadData(grantee: iam.IGrantable) { + return this.grant(grantee, ...READ_DATA_ACTIONS); + } + + /** + * 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); + } + + /** + * Permits an IAM principal all data write operations to this table: + * BatchWriteItem, PutItem, UpdateItem, DeleteItem. + * @param grantee The principal to grant access to + */ + public grantWriteData(grantee: iam.IGrantable) { + return this.grant(grantee, ...WRITE_DATA_ACTIONS); + } + + /** + * Permits an IAM principal to all data read/write operations to this table. + * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, + * BatchWriteItem, PutItem, UpdateItem, DeleteItem + * @param grantee The principal to grant access to + */ + public grantReadWriteData(grantee: iam.IGrantable) { + return this.grant(grantee, ...READ_DATA_ACTIONS, ...WRITE_DATA_ACTIONS); + } + + /** + * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. + * @param grantee The principal to grant access to + */ + public grantFullAccess(grantee: iam.IGrantable) { + return this.grant(grantee, 'dynamodb:*'); + } + + protected abstract get hasIndex(): boolean; +} /** * Provides a DynamoDB table. */ -export class Table extends Resource { +export class Table extends TableBase { /** * Permits an IAM Principal to list all DynamoDB Streams. * @deprecated Use {@link #grantTableListStreams} for more granular permission @@ -197,11 +432,69 @@ export class Table extends Resource { actions: ['dynamodb:ListStreams'], resourceArns: ['*'], }); - } + } + + /** + * Creates a Table construct that represents an external table. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param attrs A `TableAttributes` object. + */ + public static fromTableAttributes(scope: Construct, id: string, attrs: TableAttributes): ITable { + + class Import extends TableBase { + public readonly tableName = this.parseTableName(scope, attrs); + public readonly tableArn = this.parseTableArn(scope, attrs); + public readonly tableStreamArn = attrs.tableStreamArn; + + private parseTableName(construct: IConstruct, props: TableAttributes): string { + // if we have an explicit table name, use it. + if (props.tableName) { + return props.tableName; + } + + // extract table name from table arn + if (props.tableArn) { + return Stack.of(construct).parseArn(props.tableArn).resourceName!; + } + + // no table name is okay since it's optional. + throw new Error('Cannot determine table name. At least `tableArn` or `tableName` is needed'); + } + + private parseTableArn(construct: IConstruct, props: TableAttributes): string { + + // if we have an explicit table ARN, use it. + if (props.tableArn) { + return props.tableArn; + } + + if (props.tableName) { + return Stack.of(construct).formatArn({ + // DynamoDB names are globally unique in a partition, + // and so their ARNs have empty region and account components + region: '', + account: '', + service: 'dynamodb', + resource: `table/${props.tableName}` + }); + } + + throw new Error('Cannot determine table ARN. At least `tableArn` or `tableName` is needed'); + } + + protected get hasIndex(): boolean { + return attrs.hasIndex; + } + } - /** - * @attribute - */ + return new Import(scope, id); + } + + /** + * @attribute + */ public readonly tableArn: string; /** @@ -428,108 +721,6 @@ export class Table extends Resource { }); } - /** - * Adds an IAM policy statement associated with this table 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:PutItem", "dynamodb:GetItem", ...) - */ - public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - return iam.Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [ - this.tableArn, - Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }) - ], - scope: this, - }); - } - - /** - * 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. - * @param grantee The principal to grant access to - */ - public grantReadData(grantee: iam.IGrantable) { - return this.grant(grantee, ...READ_DATA_ACTIONS); - } - - /** - * 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); - } - - /** - * Permits an IAM principal all data write operations to this table: - * BatchWriteItem, PutItem, UpdateItem, DeleteItem. - * @param grantee The principal to grant access to - */ - public grantWriteData(grantee: iam.IGrantable) { - return this.grant(grantee, ...WRITE_DATA_ACTIONS); - } - - /** - * Permits an IAM principal to all data read/write operations to this table. - * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, - * BatchWriteItem, PutItem, UpdateItem, DeleteItem - * @param grantee The principal to grant access to - */ - public grantReadWriteData(grantee: iam.IGrantable) { - return this.grant(grantee, ...READ_DATA_ACTIONS, ...WRITE_DATA_ACTIONS); - } - - /** - * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. - * @param grantee The principal to grant access to - */ - public grantFullAccess(grantee: iam.IGrantable) { - return this.grant(grantee, 'dynamodb:*'); - } - /** * Validate the table construct. * @@ -553,7 +744,7 @@ export class Table extends Resource { * * @param props read and write capacity properties */ - private validateProvisioning(props: { readCapacity?: number, writeCapacity?: number}): void { + private validateProvisioning(props: { readCapacity?: number, writeCapacity?: number }): void { if (this.billingMode === BillingMode.PAY_PER_REQUEST) { if (props.readCapacity !== undefined || props.writeCapacity !== undefined) { throw new Error('you cannot provision read and write capacity for a table with PAY_PER_REQUEST billing mode'); @@ -686,7 +877,7 @@ export class Table extends Resource { /** * Whether this table has indexes */ - private get hasIndex(): boolean { + protected get hasIndex(): boolean { return this.globalSecondaryIndexes.length + this.localSecondaryIndexes.length > 0; } } @@ -740,4 +931,4 @@ export enum StreamViewType { interface ScalableAttributePair { scalableReadAttribute?: ScalableTableAttribute; scalableWriteAttribute?: ScalableTableAttribute; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index c8b4567a68796..aa46d17f6a50f 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -11,7 +11,7 @@ import { LocalSecondaryIndexProps, ProjectionType, StreamViewType, - Table + Table, } from '../lib'; // tslint:disable:object-literal-key-quotes @@ -1333,6 +1333,123 @@ export = { test.done(); } }, + + 'import': { + 'static import(ref) allows importing an external/existing table from arn'(test: Test) { + const stack = new Stack(); + + const tableArn = 'arn:aws:dynamodb:::table/MyTable'; + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableArn, + hasIndex: false, + }); + + const role = new iam.Role(stack, 'NewRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + table.grantReadData(role); + + // it is possible to obtain a permission statement for a ref + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan' + ], + "Effect": "Allow", + "Resource": [ + tableArn, + { "Ref": "AWS::NoValue" } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": 'NewRoleDefaultPolicy90E8F49D', + "Roles": [ { "Ref": 'NewRole99763075' } ] + })); + + test.deepEqual(table.tableArn, tableArn); + test.deepEqual(stack.resolve(table.tableName), 'MyTable'); + test.done(); + }, + 'static import(ref) allows importing an external/existing table from table name'(test: Test) { + const stack = new Stack(); + + const tableName = 'MyTable'; + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableName, + hasIndex: true, + }); + + const role = new iam.Role(stack, 'NewRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + table.grantReadWriteData(role); + + // it is possible to obtain a permission statement for a ref + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:DeleteItem' + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:::table/MyTable" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:::table/MyTable/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": 'NewRoleDefaultPolicy90E8F49D', + "Roles": [ { "Ref": 'NewRole99763075' } ] + })); + + test.deepEqual(table.tableArn, 'arn:${Token[AWS::Partition.3]}:dynamodb:::table/MyTable'); + test.deepEqual(stack.resolve(table.tableName), tableName); + test.done(); + }, + }, }; function testGrant(test: Test, expectedActions: string[], invocation: (user: iam.IPrincipal, table: Table) => void) {