Skip to content

Commit 7982482

Browse files
authored
Merge pull request serverless-operations#537 from bahrmichael/patch-2
feat: generate permission for dynamodb:Query and for GSIs
2 parents cb6793b + 4dfda6d commit 7982482

File tree

2 files changed

+152
-5
lines changed

2 files changed

+152
-5
lines changed

lib/deploy/stepFunctions/compileIamRole.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,31 @@ function getEcsPermissions() {
194194
}
195195

196196
function getDynamoDBPermissions(action, state) {
197-
const tableArn = state.Parameters['TableName.$']
198-
? '*'
199-
: getDynamoDBArn(state.Parameters.TableName);
197+
let resource;
198+
199+
if (state.Parameters['TableName.$']) {
200+
// When the TableName is only known at runtime, we
201+
// have to provide * permissions during deployment.
202+
resource = '*';
203+
} else if (state.Parameters['IndexName.$'] || state.Parameters.IndexName) {
204+
// When the Parameters contain an IndexName, we have to build a
205+
// longer arn that includes the index.
206+
const indexName = state.Parameters['IndexName.$']
207+
// We must provide * here instead of state.Parameters['IndexName.$'], because we don't know
208+
// which index will be targeted when we the step function runs
209+
? '*'
210+
: state.Parameters.IndexName;
211+
212+
resource = getDynamoDBArn(`${state.Parameters.TableName}/index/${indexName}`);
213+
} else {
214+
resource = getDynamoDBArn(state.Parameters.TableName);
215+
}
200216

201217
return [{
202218
action,
203-
resource: tableArn,
219+
resource,
204220
}];
205221
}
206-
207222
function getRedshiftDataPermissions(action, state) {
208223
if (['redshift-data:ExecuteStatement', 'redshift-data:BatchExecuteStatement'].includes(action)) {
209224
const clusterName = _.has(state, 'Parameters.ClusterIdentifier') ? state.Parameters.ClusterIdentifier : '*';
@@ -497,6 +512,8 @@ function getIamPermissions(taskStates) {
497512
return getDynamoDBPermissions('dynamodb:DeleteItem', state);
498513
case 'arn:aws:states:::aws-sdk:dynamodb:updateTable':
499514
return getDynamoDBPermissions('dynamodb:UpdateTable', state);
515+
case 'arn:aws:states:::aws-sdk:dynamodb:query':
516+
return getDynamoDBPermissions('dynamodb:Query', state);
500517

501518
case 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement':
502519
return getRedshiftDataPermissions('redshift-data:ExecuteStatement', state);

lib/deploy/stepFunctions/compileIamRole.test.js

+130
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,58 @@ describe('#compileIamRole', () => {
731731
.to.be.deep.equal([worldTableArn]);
732732
});
733733

734+
it('should give dynamodb permission to index table whenever IndexName is provided', () => {
735+
const helloTable = 'hello';
736+
737+
const genStateMachine = (id, tableName) => ({
738+
id,
739+
definition: {
740+
StartAt: 'A',
741+
States: {
742+
A: {
743+
Type: 'Task',
744+
Resource: 'arn:aws:states:::aws-sdk:dynamodb:query',
745+
Parameters: {
746+
TableName: tableName,
747+
},
748+
Next: 'B',
749+
},
750+
B: {
751+
Type: 'Task',
752+
Resource: 'arn:aws:states:::aws-sdk:dynamodb:query',
753+
Parameters: {
754+
TableName: tableName,
755+
IndexName: 'GSI1',
756+
},
757+
End: true,
758+
},
759+
},
760+
},
761+
});
762+
763+
serverless.service.stepFunctions = {
764+
stateMachines: {
765+
myStateMachine1: genStateMachine('StateMachine1', helloTable),
766+
},
767+
};
768+
769+
serverlessStepFunctions.compileIamRole();
770+
const policy = serverlessStepFunctions.serverless.service
771+
.provider.compiledCloudFormationTemplate.Resources.StateMachine1Role
772+
.Properties.Policies[0];
773+
774+
expect(policy.PolicyDocument.Statement[0].Action)
775+
.to.be.deep.equal(['dynamodb:Query']);
776+
777+
expect(policy.PolicyDocument.Statement[0].Resource[0]).to.be.deep.equal({
778+
'Fn::Join': [':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello']],
779+
});
780+
781+
expect(policy.PolicyDocument.Statement[0].Resource[1]).to.be.deep.equal({
782+
'Fn::Join': [':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello/index/GSI1']],
783+
});
784+
});
785+
734786
it('should give dynamodb permission to * whenever TableName.$ is seen', () => {
735787
const helloTable = 'hello';
736788

@@ -778,6 +830,84 @@ describe('#compileIamRole', () => {
778830
expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*');
779831
});
780832

833+
it('should give dynamodb permission to table/TableName/index/* when IndexName.$ is seen', () => {
834+
const helloTable = 'hello';
835+
836+
const genStateMachine = (id, tableName) => ({
837+
id,
838+
definition: {
839+
StartAt: 'A',
840+
States: {
841+
A: {
842+
Type: 'Task',
843+
Resource: 'arn:aws:states:::aws-sdk:dynamodb:query',
844+
Parameters: {
845+
TableName: tableName,
846+
'IndexName.$': '$.myDynamicIndexName',
847+
},
848+
End: true,
849+
},
850+
},
851+
},
852+
});
853+
854+
serverless.service.stepFunctions = {
855+
stateMachines: {
856+
myStateMachine1: genStateMachine('StateMachine1', helloTable),
857+
},
858+
};
859+
860+
serverlessStepFunctions.compileIamRole();
861+
const policy = serverlessStepFunctions.serverless.service
862+
.provider.compiledCloudFormationTemplate.Resources.StateMachine1Role
863+
.Properties.Policies[0];
864+
expect(policy.PolicyDocument.Statement[0].Action)
865+
.to.be.deep.equal(['dynamodb:Query']);
866+
867+
// even though some tasks target specific indices, because IndexName.$ is used we
868+
// have to give broad permissions to allow execution to talk to whatever index
869+
// the input specifies
870+
expect(policy.PolicyDocument.Statement[0].Resource[0]['Fn::Join'][1][5]).to.equal('table/hello/index/*');
871+
});
872+
873+
it('should give dynamodb permission to table/* whenever TableName.$ and IndexName.$ are seen', () => {
874+
const genStateMachine = id => ({
875+
id,
876+
definition: {
877+
StartAt: 'A',
878+
States: {
879+
A: {
880+
Type: 'Task',
881+
Resource: 'arn:aws:states:::aws-sdk:dynamodb:query',
882+
Parameters: {
883+
'TableName.$': '$.myDynamicTableName',
884+
'IndexName.$': '$.myDynamicIndexName',
885+
},
886+
End: true,
887+
},
888+
},
889+
},
890+
});
891+
892+
serverless.service.stepFunctions = {
893+
stateMachines: {
894+
myStateMachine1: genStateMachine('StateMachine1'),
895+
},
896+
};
897+
898+
serverlessStepFunctions.compileIamRole();
899+
const policy = serverlessStepFunctions.serverless.service
900+
.provider.compiledCloudFormationTemplate.Resources.StateMachine1Role
901+
.Properties.Policies[0];
902+
expect(policy.PolicyDocument.Statement[0].Action)
903+
.to.be.deep.equal(['dynamodb:Query']);
904+
905+
// even though some tasks target specific tables, because TableName.$ is used we
906+
// have to give broad permissions to allow execution to talk to whatever table
907+
// the input specifies
908+
expect(policy.PolicyDocument.Statement[0].Resource[0]).to.equal('*');
909+
});
910+
781911
it('should give Redshift Data permissions to * for safe actions', () => {
782912
serverless.service.stepFunctions = {
783913
stateMachines: {

0 commit comments

Comments
 (0)