diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index e476c4feb59ac..1b254f0d17e90 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -19,6 +19,18 @@ const table = new dynamodb.Table(this, 'Table', { }); ``` +### Importing existing tables + +To import an existing table into your CDK application, use the `Table.fromTableName` or `Table.fromTableArn` +factory method. This method accepts table name or table ARN which describes the properties of an already +existing table: + +```ts +const table = Table.fromTableArn(this, 'ImportedTable', 'arn:aws:dynamodb:us-east-1:111111111:table/my-table'); +// 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 10f47fd397ef2..8d21402acbfe9 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1,6 +1,6 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; +import { Aws, Construct, 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,170 @@ export interface LocalSecondaryIndexProps extends SecondaryIndexProps { readonly sortKey: Attribute; } +/** + * An interface that represents a DynamoDB Table - either created with the CDK, or an existing one. + */ +export interface ITable extends IResource { + /** + * Arn of the dynamodb table. + * + * @attribute + */ + readonly tableArn: string; + + /** + * Table name of the dynamodb table. + * + * @attribute + */ + readonly tableName: string; + + /** + * 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; +} + +/** + * Reference to a dynamodb table. + */ +export interface TableAttributes { + /** + * The ARN of the dynamodb table. + * One of this, or {@link tabeName}, is required. + * + * @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 + */ + readonly tableName?: string; +} + +abstract class TableBase extends Resource implements ITable { + /** + * @attribute + */ + public abstract readonly tableArn: string; + + /** + * @attribute + */ + public abstract readonly tableName: 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", ...) + */ + 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, + }); + } + + /** + * 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): iam.Grant { + 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 abstract 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 + */ + public abstract 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 + */ + public grantWriteData(grantee: iam.IGrantable): iam.Grant { + 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): iam.Grant { + 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 +357,89 @@ export class Table extends Resource { actions: ['dynamodb:ListStreams'], resourceArns: ['*'], }); - } + } + + /** + * Creates a Table construct that represents an external table via table name. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param tableName The table's name. + */ + public static fromTableName(scope: Construct, id: string, tableName: string): ITable { + return Table.fromTableAttributes(scope, id, { tableName }); + } + + /** + * Creates a Table construct that represents an external table via table arn. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param tableArn The table's ARN. + */ + public static fromTableArn(scope: Construct, id: string, tableArn: string): ITable { + return Table.fromTableAttributes(scope, id, { tableArn }); + } + + /** + * 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: string; + public readonly tableArn: string; + + constructor(_scope: Construct, _id: string, _tableArn: string, _tableName: string) { + super(_scope, _id); + this.tableArn = _tableArn; + this.tableName = _tableName; + } + + 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; + const stack = Stack.of(scope); + if (!attrs.tableName) { + if (!attrs.tableArn) { throw new Error('One of tableName or tableArn is required!'); } + + tableArn = 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; + } else { + if (attrs.tableArn) { throw new Error("Only one of tableArn or tableName can be provided"); } + tableName = attrs.tableName; + tableArn = stack.formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: attrs.tableName, + }); + } + + return new Import(scope, id, tableArn, tableName); + } - /** - * @attribute - */ + /** + * @attribute + */ public readonly tableArn: string; /** @@ -280,6 +518,54 @@ export class Table extends Resource { } } + /** + * 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. * @@ -428,108 +714,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 +737,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 +870,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 +924,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 62f3d0be8c7c8..59b1dd6a0c0fa 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,125 @@ export = { test.done(); } }, + + 'import': { + 'report error when importing an external/existing table from invalid arn missing resource name'(test: Test) { + const stack = new Stack(); + + const tableArn = 'arn:aws:dynamodb:us-east-1::table/'; + // WHEN + test.throws(() => Table.fromTableArn(stack, 'ImportedTable', tableArn), /ARN for DynamoDB table must be in the form: .../); + + test.done(); + }, + 'static import(ref) 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'; + const table = Table.fromTableArn(stack, 'ImportedTable', tableArn); + + 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.fromTableName(stack, 'ImportedTable', tableName); + + 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:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":table/MyTable" + ] + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": 'NewRoleDefaultPolicy90E8F49D', + "Roles": [ { "Ref": 'NewRole99763075' } ] + })); + + test.deepEqual(table.tableArn, 'arn:${Token[AWS::Partition.3]}:dynamodb:${Token[AWS::Region.4]}:${Token[AWS::AccountId.0]}:table/MyTable'); + test.deepEqual(stack.resolve(table.tableName), tableName); + test.done(); + }, + }, }; function testGrant(test: Test, expectedActions: string[], invocation: (user: iam.IPrincipal, table: Table) => void) {