Skip to content
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
58 changes: 47 additions & 11 deletions lib/deploy/stepFunctions/compileIamRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 [{
Expand All @@ -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 [{
Expand All @@ -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,
Expand Down
108 changes: 38 additions & 70 deletions lib/deploy/stepFunctions/compileIamRole.test.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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': [
Expand All @@ -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']),
Expand Down Expand Up @@ -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 = {
Expand All @@ -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,
},
},
Expand Down Expand Up @@ -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) => ({
Expand All @@ -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,
},
},
Expand Down Expand Up @@ -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) => ({
Expand All @@ -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,
},
},
Expand All @@ -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) => ({
Expand All @@ -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,
},
},
Expand All @@ -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: {
Expand All @@ -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,
},
},
Expand All @@ -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': [
Expand All @@ -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,
},
},
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
},
},
Expand Down
Loading