diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 7cb3e0f944423..601570ec1f2d1 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1,11 +1,27 @@ import { cloudformation as applicationautoscaling } from '@aws-cdk/aws-applicationautoscaling'; -import { PolicyStatement, PolicyStatementEffect, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import iam = require('@aws-cdk/aws-iam'); import { Construct, TagManager, Tags } from '@aws-cdk/cdk'; import { cloudformation as dynamodb } from './dynamodb.generated'; const HASH_KEY_TYPE = 'HASH'; const RANGE_KEY_TYPE = 'RANGE'; +const READ_DATA_ACTIONS = [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan' +]; + +const WRITE_DATA_ACTIONS = [ + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:DeleteItem' +]; + export interface Attribute { /** * The name of an attribute. @@ -314,6 +330,57 @@ export class Table extends Construct { this.writeScalingPolicyResource = this.buildAutoScaling(this.writeScalingPolicyResource, 'Write', props); } + /** + * Adds an IAM policy statement associated with this table to an IAM + * principal's policy. + * @param principal The principal (no-op if undefined) + * @param actions The set of actions to allow (i.e. "dynamodb:PutItem", "dynamodb:GetItem", ...) + */ + public grant(principal?: iam.IPrincipal, ...actions: string[]) { + if (!principal) { + return; + } + principal.addToPolicy(new iam.PolicyStatement() + .addResource(this.tableArn) + .addActions(...actions)); + } + + /** + * Permits an IAM principal all data read operations from this table: + * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. + * @param principal The principal to grant access to + */ + public grantReadData(principal?: iam.IPrincipal) { + this.grant(principal, ...READ_DATA_ACTIONS); + } + + /** + * Permits an IAM principal all data write operations to this table: + * BatchWriteItem, PutItem, UpdateItem, DeleteItem. + * @param principal The principal to grant access to + */ + public grantWriteData(principal?: iam.IPrincipal) { + this.grant(principal, ...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 principal The principal to grant access to + */ + public grantReadWriteData(principal?: iam.IPrincipal) { + this.grant(principal, ...READ_DATA_ACTIONS, ...WRITE_DATA_ACTIONS); + } + + /** + * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. + * @param principal The principal to grant access to + */ + public grantFullAccess(principal?: iam.IPrincipal) { + this.grant(principal, 'dynamodb:*'); + } + /** * Validate the table construct. * @@ -443,13 +510,13 @@ export class Table extends Construct { } private buildAutoScalingRole(roleResourceName: string) { - const autoScalingRole = new Role(this, roleResourceName, { - assumedBy: new ServicePrincipal('application-autoscaling.amazonaws.com') + const autoScalingRole = new iam.Role(this, roleResourceName, { + assumedBy: new iam.ServicePrincipal('application-autoscaling.amazonaws.com') }); - autoScalingRole.addToPolicy(new PolicyStatement(PolicyStatementEffect.Allow) + autoScalingRole.addToPolicy(new iam.PolicyStatement(iam.PolicyStatementEffect.Allow) .addActions("dynamodb:DescribeTable", "dynamodb:UpdateTable") .addResource(this.tableArn)); - autoScalingRole.addToPolicy(new PolicyStatement(PolicyStatementEffect.Allow) + autoScalingRole.addToPolicy(new iam.PolicyStatement(iam.PolicyStatementEffect.Allow) .addActions("cloudwatch:PutMetricAlarm", "cloudwatch:DescribeAlarms", "cloudwatch:GetMetricStatistics", "cloudwatch:SetAlarmState", "cloudwatch:DeleteAlarms") .addAllResources()); @@ -457,7 +524,7 @@ export class Table extends Construct { } private buildScalableTargetResourceProps(scalableDimension: string, - scalingRole: Role, + scalingRole: iam.Role, props: AutoScalingProps) { return { maxCapacity: props.maxCapacity, diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index e605d1ab816b7..8516074446576 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1,3 +1,5 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { @@ -10,6 +12,8 @@ import { Table } from '../lib'; +// tslint:disable:object-literal-key-quotes + // CDK parameters const STACK_NAME = 'MyStack'; const CONSTRUCT_NAME = 'MyTable'; @@ -2024,6 +2028,34 @@ export = { }), /minimumCapacity must be greater than or equal to 0; Provided value is: -5/); test.done(); + }, + + 'grants': { + + '"grant" allows adding arbitrary actions associated with this table resource'(test: Test) { + testGrant(test, + [ 'action1', 'action2' ], (p, t) => t.grant(p, 'dynamodb:action1', 'dynamodb:action2')); + }, + + '"grantReadData" allows the principal to read data from the table'(test: Test) { + testGrant(test, + [ 'BatchGetItem', 'GetRecords', 'GetShardIterator', 'Query', 'GetItem', 'Scan' ], (p, t) => t.grantReadData(p)); + }, + + '"grantWriteData" allows the principal to write data to the table'(test: Test) { + testGrant(test, [ + 'BatchWriteItem', 'PutItem', 'UpdateItem', 'DeleteItem' ], (p, t) => t.grantWriteData(p)); + }, + + '"grantReadWriteData" allows the principal to read/write data'(test: Test) { + testGrant(test, [ + 'BatchGetItem', 'GetRecords', 'GetShardIterator', 'Query', 'GetItem', 'Scan', + 'BatchWriteItem', 'PutItem', 'UpdateItem', 'DeleteItem' ], (p, t) => t.grantReadWriteData(p)); + }, + + '"grantFullAccess" allows the principal to perform any action on the table ("*")'(test: Test) { + testGrant(test, [ '*' ], (p, t) => t.grantFullAccess(p)); + } } }; @@ -2036,3 +2068,38 @@ class TestApp { return this.app.synthesizeStack(this.stack.name).template; } } + +function testGrant(test: Test, expectedActions: string[], invocation: (user: iam.IPrincipal, table: Table) => void) { + // GIVEN + const stack = new Stack(); + + const table = new Table(stack, 'my-table'); + table.addPartitionKey({ name: 'ID', type: AttributeType.String }); + + const user = new iam.User(stack, 'user'); + + // WHEN + invocation(user, table); + + // THEN + const action = expectedActions.length > 1 ? expectedActions.map(a => `dynamodb:${a}`) : `dynamodb:${expectedActions[0]}`; + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": action, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "mytable0324D45C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Users": [ { "Ref": "user2C2B57AE" } ] + })); + test.done(); +} \ No newline at end of file