diff --git a/lib/deploy/stepFunctions/compileIamRole.js b/lib/deploy/stepFunctions/compileIamRole.js index 864491b..8772a20 100644 --- a/lib/deploy/stepFunctions/compileIamRole.js +++ b/lib/deploy/stepFunctions/compileIamRole.js @@ -196,25 +196,61 @@ function getEcsPermissions() { }]; } +function isJsonPathParameter(state, key) { + const jsonPath = `${key}.$`; + return state.Parameters && state.Parameters[jsonPath]; +} + +function isJsonataArgument(state, key) { + return state.Arguments && state.Arguments[key] && typeof state.Arguments[key] === 'string' && state.Arguments[key].trim().startsWith('{%'); +} + +function getParameterOrArgument(state, key) { + if (state.QueryLanguage === 'JSONata') return state.Arguments && state.Arguments[key]; + + if (state.QueryLanguage === 'JSONPath') return state.Parameters && state.Parameters[key]; + + if (state.Parameters && !state.Arguments) return state.Parameters[key]; + + if (state.Arguments && !state.Parameters) return state.Arguments[key]; + + return undefined; +} + +function hasParameterOrArgument(state, key) { + if (state.QueryLanguage === 'JSONata') return state.Arguments && state.Arguments[key]; + + if (state.QueryLanguage === 'JSONPath') return state.Parameters && state.Parameters[key]; + + // If no query language is specified, we would need to go to the top-level definition + // and check if the key is present at the state machine definition + // As workaround, we will simply check if eitehr Parameters or Arguments is present + if (state.Parameters && !state.Arguments) return state.Parameters[key]; + + if (state.Arguments && !state.Parameters) return state.Arguments[key]; + + return false; +} + function getDynamoDBPermissions(action, state) { let resource; - if (state.Parameters['TableName.$']) { + if (isJsonPathParameter(state, 'TableName') || isJsonataArgument(state, 'TableName')) { // When the TableName is only known at runtime, we // have to provide * permissions during deployment. resource = '*'; - } else if (state.Parameters['IndexName.$'] || state.Parameters.IndexName) { + } else if (isJsonPathParameter(state, 'IndexName') || isJsonataArgument(state, 'IndexName')) { + // We must provide * here instead of state.Parameters['IndexName.$'], because we don't know + // which index will be targeted when we the step function runs + resource = getDynamoDBArn(`${getParameterOrArgument(state, 'TableName')}/index/*`); + } else if (hasParameterOrArgument(state, 'IndexName')) { // When the Parameters contain an IndexName, we have to build a // longer arn that includes the index. - const indexName = state.Parameters['IndexName.$'] - // We must provide * here instead of state.Parameters['IndexName.$'], because we don't know - // which index will be targeted when we the step function runs - ? '*' - : state.Parameters.IndexName; + const indexName = getParameterOrArgument(state, 'IndexName'); - resource = getDynamoDBArn(`${state.Parameters.TableName}/index/${indexName}`); + resource = getDynamoDBArn(`${getParameterOrArgument(state, 'TableName')}/index/${indexName}`); } else { - resource = getDynamoDBArn(state.Parameters.TableName); + resource = getDynamoDBArn(getParameterOrArgument(state, 'TableName')); } return [{ @@ -224,7 +260,7 @@ function getDynamoDBPermissions(action, state) { } function getBatchDynamoDBPermissions(action, state) { - if (state.Parameters['RequestItems.$']) { + if (isJsonPathParameter(state, 'RequestItems') || isJsonataArgument(state, 'RequestItems')) { // When the RequestItems object is only known at runtime, // we have to provide * permissions during deployment. return [{ @@ -236,7 +272,7 @@ function getBatchDynamoDBPermissions(action, state) { // table names as keys. We can use these to generate roles // whether the array of requests for that table is known // at deploy time or not - const tableNames = Object.keys(state.Parameters.RequestItems); + const tableNames = Object.keys(getParameterOrArgument(state, 'RequestItems')); return tableNames.map(tableName => ({ action, diff --git a/lib/deploy/stepFunctions/compileIamRole.test.js b/lib/deploy/stepFunctions/compileIamRole.test.js index d9f76a2..e6603ef 100644 --- a/lib/deploy/stepFunctions/compileIamRole.test.js +++ b/lib/deploy/stepFunctions/compileIamRole.test.js @@ -1,12 +1,19 @@ 'use strict'; const _ = require('lodash'); +const itParam = require('mocha-param'); const expect = require('chai').expect; const sinon = require('sinon'); const Serverless = require('serverless/lib/Serverless'); const AwsProvider = require('serverless/lib/plugins/aws/provider'); const ServerlessStepFunctions = require('./../../index'); +function getParamsOrArgs(queryLanguage, params, args) { + return queryLanguage === 'JSONPath' + ? { Parameters: params } + : { Arguments: args === undefined ? params : args }; +} + describe('#compileIamRole', () => { let serverless; let serverlessStepFunctions; @@ -536,7 +543,7 @@ describe('#compileIamRole', () => { expect(policy.PolicyDocument.Statement[0].Resource).to.have.lengthOf(0); }); - it('should give dynamodb permission for only tables referenced by state machine', () => { + itParam('should give dynamodb permission for only tables referenced by state machine: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const helloTableArn = { 'Fn::Join': [ @@ -554,50 +561,42 @@ describe('#compileIamRole', () => { id, definition: { StartAt: 'A', + QueryLanguage: queryLanguage, States: { A: { Type: 'Task', Resource: resources[0], - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: resources[1], - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'C', }, C: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:getItem', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'D', }, D: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:deleteItem', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, E: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:updateTable', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, }, }, }); + serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable, ['arn:aws:states:::dynamodb:updateItem', 'arn:aws:states:::dynamodb:putItem']), @@ -636,7 +635,7 @@ describe('#compileIamRole', () => { .to.be.deep.equal([worldTableArn]); }); - it('should give dynamodb permission for table name imported from external stack', () => { + itParam('should give dynamodb permission for table name imported from external stack', ['JSONPath', 'JSONata'], (queryLanguage) => { // Necessary to convince the region is in the gov cloud infrastructure. const externalHelloTable = { 'Fn::ImportValue': 'HelloStack:Table:Name' }; const helloTableArn = { @@ -660,33 +659,25 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: resources[0], - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: resources[1], - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'C', }, C: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:getItem', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'D', }, D: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:deleteItem', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, }, @@ -731,7 +722,7 @@ describe('#compileIamRole', () => { .to.be.deep.equal([worldTableArn]); }); - it('should give dynamodb permission to index table whenever IndexName is provided', () => { + itParam('should give dynamodb permission to index table whenever IndexName is provided: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ @@ -742,18 +733,13 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', - Parameters: { - TableName: tableName, - IndexName: 'GSI1', - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName, IndexName: 'GSI1' }), End: true, }, }, @@ -783,7 +769,7 @@ describe('#compileIamRole', () => { }); }); - it('should give dynamodb permission to * whenever TableName.$ is seen', () => { + itParam('should give dynamodb permission to * whenever TableName.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ @@ -794,17 +780,13 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:updateItem', - Parameters: { - TableName: tableName, - }, + ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:updateItem', - Parameters: { - 'TableName.$': '$.tableName', - }, + ...getParamsOrArgs(queryLanguage, { 'TableName.$': '$.tableName' }, { TableName: '{% $tableName %}' }), End: true, }, }, @@ -830,7 +812,7 @@ describe('#compileIamRole', () => { expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); - it('should give dynamodb permission to table/TableName/index/* when IndexName.$ is seen', () => { + itParam('should give dynamodb permission to table/TableName/index/* when IndexName.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ @@ -841,10 +823,10 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', - Parameters: { + ...getParamsOrArgs(queryLanguage, { TableName: tableName, 'IndexName.$': '$.myDynamicIndexName', - }, + }, { TableName: tableName, IndexName: '{% $myDynamicIndexName %}' }), End: true, }, }, @@ -870,7 +852,7 @@ describe('#compileIamRole', () => { expect(policy.PolicyDocument.Statement[0].Resource[0]['Fn::Join'][1][5]).to.equal('table/hello/index/*'); }); - it('should give dynamodb permission to table/* whenever TableName.$ and IndexName.$ are seen', () => { + itParam('should give dynamodb permission to table/* whenever TableName.$ and IndexName.$ are seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const genStateMachine = id => ({ id, definition: { @@ -879,10 +861,10 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', - Parameters: { + ...getParamsOrArgs(queryLanguage, { 'TableName.$': '$.myDynamicTableName', 'IndexName.$': '$.myDynamicIndexName', - }, + }, { TableName: '{% $myDynamicTableName %}', IndexName: '{% $myDynamicIndexName %}' }), End: true, }, }, @@ -908,7 +890,7 @@ describe('#compileIamRole', () => { expect(policy.PolicyDocument.Statement[0].Resource[0]).to.equal('*'); }); - it('should give batch dynamodb permission for only tables referenced by state machine', () => { + itParam('should give batch dynamodb permission for only tables referenced by state machine: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const helloTableArn = { 'Fn::Join': [ @@ -930,21 +912,13 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', - Parameters: { - RequestItems: { - [tableName]: [], - }, - }, + ...getParamsOrArgs(queryLanguage, { RequestItems: { [tableName]: [] } }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchGetItem', - Parameters: { - RequestItems: { - [tableName]: {}, - }, - }, + ...getParamsOrArgs(queryLanguage, { RequestItems: { [tableName]: {} } }), End: true, }, }, @@ -977,7 +951,7 @@ describe('#compileIamRole', () => { .to.be.deep.equal([worldTableArn]); }); - it('should give batch dynamodb permission to * whenever RequestItems.$ is seen', () => { + itParam('should give batch dynamodb permission to * whenever RequestItems.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const genStateMachine = id => ({ id, definition: { @@ -986,19 +960,13 @@ describe('#compileIamRole', () => { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', - Parameters: { - RequestItems: { - tableName: [], - }, - }, + ...getParamsOrArgs(queryLanguage, { RequestItems: { tableName: [] } }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', - Parameters: { - 'RequestItems.$': '$.requestItems', - }, + ...getParamsOrArgs(queryLanguage, { 'RequestItems.$': '$.requestItems' }, { RequestItems: '{% $requestItems %}' }), End: true, }, },