Skip to content

Commit 33c2a6d

Browse files
authored
fix(dynamodb): grant also gives access to indexes (#1564)
If a table has an index, the `grant()` methods will give permissions to the index as well. Updates the security diff engine to support `AWS::NoValue`. Fixes #1540.
1 parent 6d0cdc7 commit 33c2a6d

File tree

6 files changed

+198
-29
lines changed

6 files changed

+198
-29
lines changed

packages/@aws-cdk/aws-dynamodb/lib/table.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ export class Table extends Construct {
429429
return;
430430
}
431431
principal.addToPolicy(new iam.PolicyStatement()
432-
.addResource(this.tableArn)
432+
.addResources(this.tableArn, new cdk.Token(() => this.hasIndex ? `${this.tableArn}/index/*` : new cdk.Aws().noValue).toString())
433433
.addActions(...actions));
434434
}
435435

@@ -622,6 +622,13 @@ export class Table extends Construct {
622622
})
623623
});
624624
}
625+
626+
/**
627+
* Whether this table has indexes
628+
*/
629+
private get hasIndex(): boolean {
630+
return this.globalSecondaryIndexes.length + this.localSecondaryIndexes.length > 0;
631+
}
625632
}
626633

627634
export enum AttributeType {

packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json

+92-18
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
"KeyType": "HASH"
1010
}
1111
],
12-
"ProvisionedThroughput": {
13-
"ReadCapacityUnits": 5,
14-
"WriteCapacityUnits": 5
15-
},
1612
"AttributeDefinitions": [
1713
{
1814
"AttributeName": "hashKey",
1915
"AttributeType": "S"
2016
}
21-
]
17+
],
18+
"ProvisionedThroughput": {
19+
"ReadCapacityUnits": 5,
20+
"WriteCapacityUnits": 5
21+
}
2222
}
2323
},
2424
"TableWithGlobalAndLocalSecondaryIndexBC540710": {
@@ -34,10 +34,6 @@
3434
"KeyType": "RANGE"
3535
}
3636
],
37-
"ProvisionedThroughput": {
38-
"ReadCapacityUnits": 5,
39-
"WriteCapacityUnits": 5
40-
},
4137
"AttributeDefinitions": [
4238
{
4339
"AttributeName": "hashKey",
@@ -251,6 +247,10 @@
251247
"PointInTimeRecoverySpecification": {
252248
"PointInTimeRecoveryEnabled": true
253249
},
250+
"ProvisionedThroughput": {
251+
"ReadCapacityUnits": 5,
252+
"WriteCapacityUnits": 5
253+
},
254254
"SSESpecification": {
255255
"SSEEnabled": true
256256
},
@@ -278,10 +278,6 @@
278278
"KeyType": "HASH"
279279
}
280280
],
281-
"ProvisionedThroughput": {
282-
"ReadCapacityUnits": 5,
283-
"WriteCapacityUnits": 5
284-
},
285281
"AttributeDefinitions": [
286282
{
287283
"AttributeName": "hashKey",
@@ -309,7 +305,11 @@
309305
"WriteCapacityUnits": 5
310306
}
311307
}
312-
]
308+
],
309+
"ProvisionedThroughput": {
310+
"ReadCapacityUnits": 5,
311+
"WriteCapacityUnits": 5
312+
}
313313
}
314314
},
315315
"TableWithLocalSecondaryIndex4DA3D08F": {
@@ -325,10 +325,6 @@
325325
"KeyType": "RANGE"
326326
}
327327
],
328-
"ProvisionedThroughput": {
329-
"ReadCapacityUnits": 5,
330-
"WriteCapacityUnits": 5
331-
},
332328
"AttributeDefinitions": [
333329
{
334330
"AttributeName": "hashKey",
@@ -360,6 +356,84 @@
360356
"ProjectionType": "ALL"
361357
}
362358
}
359+
],
360+
"ProvisionedThroughput": {
361+
"ReadCapacityUnits": 5,
362+
"WriteCapacityUnits": 5
363+
}
364+
}
365+
},
366+
"User00B015A1": {
367+
"Type": "AWS::IAM::User"
368+
},
369+
"UserDefaultPolicy1F97781E": {
370+
"Type": "AWS::IAM::Policy",
371+
"Properties": {
372+
"PolicyDocument": {
373+
"Statement": [
374+
{
375+
"Action": [
376+
"dynamodb:BatchGetItem",
377+
"dynamodb:GetRecords",
378+
"dynamodb:GetShardIterator",
379+
"dynamodb:Query",
380+
"dynamodb:GetItem",
381+
"dynamodb:Scan"
382+
],
383+
"Effect": "Allow",
384+
"Resource": [
385+
{
386+
"Fn::GetAtt": [
387+
"TableCD117FA1",
388+
"Arn"
389+
]
390+
},
391+
{
392+
"Ref": "AWS::NoValue"
393+
}
394+
]
395+
},
396+
{
397+
"Action": [
398+
"dynamodb:BatchGetItem",
399+
"dynamodb:GetRecords",
400+
"dynamodb:GetShardIterator",
401+
"dynamodb:Query",
402+
"dynamodb:GetItem",
403+
"dynamodb:Scan"
404+
],
405+
"Effect": "Allow",
406+
"Resource": [
407+
{
408+
"Fn::GetAtt": [
409+
"TableWithGlobalAndLocalSecondaryIndexBC540710",
410+
"Arn"
411+
]
412+
},
413+
{
414+
"Fn::Join": [
415+
"",
416+
[
417+
{
418+
"Fn::GetAtt": [
419+
"TableWithGlobalAndLocalSecondaryIndexBC540710",
420+
"Arn"
421+
]
422+
},
423+
"/index/*"
424+
]
425+
]
426+
}
427+
]
428+
}
429+
],
430+
"Version": "2012-10-17"
431+
},
432+
"PolicyName": "UserDefaultPolicy1F97781E",
433+
"Users": [
434+
{
435+
"Ref": "User00B015A1"
436+
}
363437
]
364438
}
365439
}

packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import iam = require('@aws-cdk/aws-iam');
12
import { App, Stack } from '@aws-cdk/cdk';
23
import { Attribute, AttributeType, ProjectionType, StreamViewType, Table } from '../lib';
34

@@ -118,4 +119,8 @@ tableWithLocalSecondaryIndex.addLocalSecondaryIndex({
118119
sortKey: LSI_SORT_KEY
119120
});
120121

122+
const user = new iam.User(stack, 'User');
123+
table.grantReadData(user);
124+
tableWithGlobalAndLocalSecondaryIndex.grantReadData(user);
125+
121126
app.run();

packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts

+44-7
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,46 @@ export = {
13531353

13541354
'"grantFullAccess" allows the principal to perform any action on the table ("*")'(test: Test) {
13551355
testGrant(test, [ '*' ], (p, t) => t.grantFullAccess(p));
1356-
}
1356+
},
1357+
1358+
'if table has an index grant gives access to the index'(test: Test) {
1359+
// GIVEN
1360+
const stack = new Stack();
1361+
1362+
const table = new Table(stack, 'my-table');
1363+
table.addPartitionKey({ name: 'ID', type: AttributeType.String });
1364+
table.addGlobalSecondaryIndex({ indexName: 'MyIndex', partitionKey: { name: 'Age', type: AttributeType.Number }});
1365+
const user = new iam.User(stack, 'user');
1366+
1367+
// WHEN
1368+
table.grantReadData(user);
1369+
1370+
// THEN
1371+
expect(stack).to(haveResource('AWS::IAM::Policy', {
1372+
"PolicyDocument": {
1373+
"Statement": [
1374+
{
1375+
"Action": [
1376+
'dynamodb:BatchGetItem',
1377+
'dynamodb:GetRecords',
1378+
'dynamodb:GetShardIterator',
1379+
'dynamodb:Query',
1380+
'dynamodb:GetItem',
1381+
'dynamodb:Scan'
1382+
],
1383+
"Effect": "Allow",
1384+
"Resource": [
1385+
{ "Fn::GetAtt": ["mytable0324D45C", "Arn"] },
1386+
{ "Fn::Join": [ "", [ { "Fn::GetAtt": [ "mytable0324D45C", "Arn" ] }, "/index/*" ] ] }
1387+
]
1388+
}
1389+
],
1390+
"Version": "2012-10-17"
1391+
},
1392+
}));
1393+
test.done();
1394+
},
1395+
13571396
},
13581397
};
13591398

@@ -1387,12 +1426,10 @@ function testGrant(test: Test, expectedActions: string[], invocation: (user: iam
13871426
{
13881427
"Action": action,
13891428
"Effect": "Allow",
1390-
"Resource": {
1391-
"Fn::GetAtt": [
1392-
"mytable0324D45C",
1393-
"Arn"
1394-
]
1395-
}
1429+
"Resource": [
1430+
{ "Fn::GetAtt": [ "mytable0324D45C", "Arn" ] },
1431+
{ "Ref" : "AWS::NoValue" }
1432+
]
13961433
}
13971434
],
13981435
"Version": "2012-10-17"

packages/@aws-cdk/cloudformation-diff/lib/render-intrinsics.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
*
1414
* Evaluates Fn::Join directly if the second argument is a literal list of strings.
1515
*
16+
* Removes list and object values evaluating to { Ref: 'AWS::NoValue' }.
17+
*
1618
* For other intrinsics we choose a string representation that CloudFormation
1719
* cannot actually parse, but is comprehensible to humans.
1820
*/
1921
export function renderIntrinsics(x: any): any {
2022
if (Array.isArray(x)) {
21-
return x.map(renderIntrinsics);
23+
return x.filter(el => !isNoValue(el)).map(renderIntrinsics);
2224
}
2325

26+
if (isNoValue(x)) { return undefined; }
27+
2428
const intrinsic = getIntrinsic(x);
2529
if (intrinsic) {
2630
if (intrinsic.fn === 'Ref') { return '${' + intrinsic.args + '}'; }
@@ -32,7 +36,9 @@ export function renderIntrinsics(x: any): any {
3236
if (typeof x === 'object' && x !== null) {
3337
const ret: any = {};
3438
for (const [key, value] of Object.entries(x)) {
35-
ret[key] = renderIntrinsics(value);
39+
if (!isNoValue(value)) {
40+
ret[key] = renderIntrinsics(value);
41+
}
3642
}
3743
return ret;
3844
}
@@ -41,7 +47,7 @@ export function renderIntrinsics(x: any): any {
4147

4248
function unCloudFormationFnJoin(separator: string, args: any) {
4349
if (Array.isArray(args)) {
44-
return args.map(renderIntrinsics).join(separator);
50+
return args.filter(el => !isNoValue(el)).map(renderIntrinsics).join(separator);
4551
}
4652
return stringifyIntrinsic('Fn::Join', [separator, args]);
4753
}
@@ -57,6 +63,11 @@ function getIntrinsic(x: any): Intrinsic | undefined {
5763
return keys.length === 1 && (keys[0] === 'Ref' || keys[0].startsWith('Fn::')) ? { fn: keys[0], args: x[keys[0]] } : undefined;
5864
}
5965

66+
function isNoValue(x: any) {
67+
const int = getIntrinsic(x);
68+
return int && int.fn === 'Ref' && int.args === 'AWS::NoValue';
69+
}
70+
6071
interface Intrinsic {
6172
fn: string;
6273
args: any;

packages/@aws-cdk/cloudformation-diff/test/test.render-intrinsics.ts

+35
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ export = {
2727
test.done();
2828
},
2929

30+
'removes AWS::NoValue from Fn::Join'(test: Test) {
31+
test.equals(
32+
renderIntrinsics({ 'Fn::Join': ['/', ['a', { Ref: 'AWS::NoValue' }, 'b', 'c']] }),
33+
'a/b/c'
34+
);
35+
36+
test.done();
37+
},
38+
3039
'does not resolve Fn::Join if the second argument is not a list literal'(test: Test) {
3140
test.equals(
3241
renderIntrinsics({ 'Fn::Join': ['/', { Ref: 'ListParameter' }] }),
@@ -63,4 +72,30 @@ export = {
6372
);
6473
test.done();
6574
},
75+
76+
'removes NoValue from object'(test: Test) {
77+
test.deepEqual(
78+
renderIntrinsics({
79+
Deeper1: { Ref: 'SomeLogicalId' },
80+
Deeper2: { Ref: 'AWS::NoValue' }
81+
}),
82+
{
83+
Deeper1: '${SomeLogicalId}',
84+
}
85+
);
86+
test.done();
87+
},
88+
89+
'removes NoValue from array'(test: Test) {
90+
test.deepEqual(
91+
renderIntrinsics([
92+
{ Ref: 'SomeLogicalId' },
93+
{ Ref: 'AWS::NoValue' },
94+
]),
95+
[
96+
'${SomeLogicalId}',
97+
]
98+
);
99+
test.done();
100+
},
66101
};

0 commit comments

Comments
 (0)