Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-dynamodb): IAM grants support #870

Merged
merged 1 commit into from
Oct 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 73 additions & 6 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -443,21 +510,21 @@ 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)
eladb marked this conversation as resolved.
Show resolved Hide resolved
.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());
return autoScalingRole;
}

private buildScalableTargetResourceProps(scalableDimension: string,
scalingRole: Role,
scalingRole: iam.Role,
props: AutoScalingProps) {
return {
maxCapacity: props.maxCapacity,
Expand Down
67 changes: 67 additions & 0 deletions packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,6 +12,8 @@ import {
Table
} from '../lib';

// tslint:disable:object-literal-key-quotes

// CDK parameters
const STACK_NAME = 'MyStack';
const CONSTRUCT_NAME = 'MyTable';
Expand Down Expand Up @@ -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));
}
}
};

Expand All @@ -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();
}