UNPKG

serverless-step-functions

Version:

The module is AWS Step Functions plugin for Serverless Framework

1,401 lines (1,292 loc) 138 kB
'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; beforeEach(() => { serverless = new Serverless(); serverless.servicePath = true; serverless.service.service = 'step-functions'; serverless.configSchemaHandler = { // eslint-disable-next-line no-unused-vars defineTopLevelProperty: (propertyName, propertySchema) => {}, }; serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; serverless.setProvider('aws', new AwsProvider(serverless)); serverless.cli = { consoleLog: sinon.spy() }; const options = { stage: 'dev', region: 'ap-northeast-1', }; serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); }); const expectDenyAllPolicy = (policy) => { const statements = policy.PolicyDocument.Statement; expect(statements).to.have.lengthOf(1); expect(statements[0].Effect).to.equal('Deny'); expect(statements[0].Action).to.equal('*'); expect(statements[0].Resource).to.equal('*'); }; const getAlias = functionArn => ({ 'Fn::Sub': [ '${functionArn}:*', { functionArn, }, ], }); it('should do nothing when role property exists in all statmachine properties', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachine1', definition: 'definition', role: 'role', }, myStateMachine2: { name: 'stateMachine2', definition: 'definition', role: 'role', }, }, }; serverlessStepFunctions.compileIamRole(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources).to.deep.equal({}); }); it('should replace [region] and [policyname] with corresponding values', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine2: { name: 'stateMachine2', definition: 'definition', }, }, }; serverlessStepFunctions.compileIamRole(); const resources = Object.values(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources); expect(resources).to.have.length(1); const iamRole = resources[0]; expect(iamRole.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service) .to.deep.eq({ 'Fn::Sub': 'states.${AWS::Region}.amazonaws.com' }); expect(iamRole.Properties.Policies[0].PolicyName) .to.be.equal('dev-step-functions-statemachine'); }); it('should create corresponding resources when role property are not given', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'StateMachine1', name: 'stateMachine1', definition: 'definition', role: 'role', }, myStateMachine2: { id: 'StateMachine2', name: 'stateMachine2', definition: 'definition', }, }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless .service.provider.compiledCloudFormationTemplate.Resources; expect(Object.values(resources)).to.have.length(1); expect(resources).to.haveOwnProperty('StateMachine2Role'); expect(resources.StateMachine2Role.Type).to.equal('AWS::IAM::Role'); }); it('should give invokeFunction permission for only functions referenced by state machine', () => { const helloLambda = 'arn:aws:lambda:123:*:function:hello'; const worldLambda = 'arn:aws:lambda:*:*:function:world'; const fooLambda = 'arn:aws:lambda:us-west-2::function:foo_'; const barLambda = 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:bar'; const genStateMachine = (id, lambda1, lambda2) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: lambda1, Next: 'B', }, B: { Type: 'Task', Resource: lambda2, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloLambda, worldLambda), myStateMachine2: genStateMachine('StateMachine2', fooLambda, barLambda), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless .service.provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; expect(policy1.PolicyDocument.Statement[0].Action).to.deep.equal(['lambda:InvokeFunction']); const policy2 = resources.StateMachine2Role.Properties.Policies[0]; expect(policy2.PolicyDocument.Statement[0].Action).to.deep.equal(['lambda:InvokeFunction']); const expectation = (policy, functions) => { const policyResources = policy.PolicyDocument.Statement[0].Resource; expect(policyResources).to.have.lengthOf(4); expect(policyResources).to.include.members(functions); const versionResources = policyResources.filter(x => x['Fn::Sub']); versionResources.forEach((x) => { const template = x['Fn::Sub'][0]; expect(template).to.equal('${functionArn}:*'); }); const versionedArns = versionResources.map(x => x['Fn::Sub'][1].functionArn); expect(versionedArns).to.deep.equal(functions); }; expectation(policy1, [helloLambda, worldLambda]); expectation(policy2, [fooLambda, barLambda]); }); it('should add discrete iam role permissions', () => { const iamRoleStatement = { Effect: 'Allow', Action: ['service:Action'], Resource: ['arn:aws:item'], }; const lambdaArn = 'arn:aws:lambda:123:*:function:hello'; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'StateMachine1', name: 'stateMachine1', iamRoleStatements: [iamRoleStatement], definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: lambdaArn, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const policies = serverlessStepFunctions.serverless .service.provider.compiledCloudFormationTemplate.Resources .StateMachine1Role.Properties.Policies; expect(policies).to.have.length(1); const statements = policies[0].PolicyDocument.Statement; expect(statements).to.have.length(2); const [statement1, statement2] = statements; expect(statement1.Action[0]).to.equal('lambda:InvokeFunction'); expect(statement2).to.be.deep.equal(iamRoleStatement); }); it('should give sns:Publish permission for only SNS topics referenced by state machine', () => { const helloTopic = 'arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:hello'; const worldTopic = 'arn:aws:sns:us-east-1:#{AWS::AccountId}:world'; const genStateMachine = (id, snsTopic) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { Message: '42', TopicArn: snsTopic, }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTopic), myStateMachine2: genStateMachine('StateMachine2', worldTopic), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; const policy2 = resources.StateMachine2Role.Properties.Policies[0]; expect(policy1.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTopic]); expect(policy2.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTopic]); }); it('should give sns:Publish permission to * whenever TopicArn.$ is seen', () => { const helloTopic = 'arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:hello'; const genStateMachine = (id, snsTopic) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { Message: '42', TopicArn: snsTopic, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { Message: '42', 'TopicArn.$': '$.snsTopic', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine: genStateMachine('StateMachine1', helloTopic), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; // even though some tasks target specific topic ARNs, but because some other states // use TopicArn.$ we need to give broad permissions to be able to publish to any // topic that the input specifies expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); it('should not give sns:Publish permission if TopicArn and TopicArn.$ are missing', () => { const genStateMachine = id => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { MessageBody: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1'), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expectDenyAllPolicy(policy); }); it('should give sqs:SendMessage permission for only SQS referenced by state machine', () => { const helloQueue = 'https://sqs.#{AWS::Region}.amazonaws.com/#{AWS::AccountId}/hello'; const helloQueueArn = 'arn:aws:sqs:#{AWS::Region}:#{AWS::AccountId}:hello'; const worldQueue = 'https://sqs.us-east-1.amazonaws.com/#{AWS::AccountId}/world'; const worldQueueArn = 'arn:aws:sqs:us-east-1:#{AWS::AccountId}:world'; const govQueue = 'https://sqs.us-gov-east-1.amazonaws.com/#{AWS::AccountId}/cloudGov'; const govQueueArn = 'arn:aws-us-gov:sqs:us-gov-east-1:#{AWS::AccountId}:cloudGov'; const genStateMachine = (id, queueUrl) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: queueUrl, Message: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloQueue), myStateMachine2: genStateMachine('StateMachine2', worldQueue), myStateMachine3: genStateMachine('StateMachine3', govQueue), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; const policy2 = resources.StateMachine2Role.Properties.Policies[0]; const policy3 = resources.StateMachine3Role.Properties.Policies[0]; expect(policy1.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloQueueArn]); expect(policy2.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldQueueArn]); expect(policy3.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([govQueueArn]); }); it('should give sqs:SendMessage permission to * whenever QueueUrl.$ is seen', () => { const helloQueue = 'https://sqs.#{AWS::Region}.amazonaws.com/#{AWS::AccountId}/hello'; const genStateMachine = (id, queueUrl) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: queueUrl, Message: '42', }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { 'QueueUrl.$': '$.queueUrl', Message: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloQueue), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; // even if some tasks are targetting specific queues, because QueueUrl.$ is seen // we need to give broad permissions allow the queue URL to be specified by input expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); it('should give sqs:SendMessage permission to * whenever QueueUrl is some intrinsic function except Ref', () => { const helloQueue = 'https://sqs.#{AWS::Region}.amazonaws.com/#{AWS::AccountId}/hello'; const genStateMachine = (id, queueUrl) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: queueUrl, Message: '42', }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: { 'Fn::ImportValue': 'some-shared-value-here', }, Message: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloQueue), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; // when using instrinct functions other than Ref to define QueueUrl // we can't recontruct ARN from it, so we need to give broad permissions expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); it('should not give sqs:SendMessage permission if QueueUrl and QueueUrl.$ are missing', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { Message: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1'), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expectDenyAllPolicy(policy); }); it('should not give sqs:SendMessage permission if QueueUrl is invalid', () => { const invalidQueueUrl = 'https://sqs.us-east-1.amazonaws.com/hello'; const genStateMachine = id => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: invalidQueueUrl, Message: '42', }, End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1'), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Resource).to.have.lengthOf(0); }); itParam('should give dynamodb permission for only tables referenced by state machine: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const helloTableArn = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello'], ], }; const worldTable = 'world'; const worldTableArn = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/world'], ], }; const genStateMachine = (id, tableName, resources) => ({ id, definition: { StartAt: 'A', QueryLanguage: queryLanguage, States: { A: { Type: 'Task', Resource: resources[0], ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: resources[1], ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'C', }, C: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:getItem', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'D', }, D: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:deleteItem', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, E: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:updateTable', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable, ['arn:aws:states:::dynamodb:updateItem', 'arn:aws:states:::dynamodb:putItem']), myStateMachine2: genStateMachine('StateMachine2', worldTable, ['arn:aws:states:::dynamodb:updateItem', 'arn:aws:states:::dynamodb:putItem']), myStateMachine3: genStateMachine('StateMachine3', helloTable, ['arn:aws:states:::aws-sdk:dynamodb:updateItem', 'arn:aws:states:::aws-sdk:dynamodb:putItem']), myStateMachine4: genStateMachine('StateMachine4', worldTable, ['arn:aws:states:::aws-sdk:dynamodb:updateItem.waitForTaskToken', 'arn:aws:states:::aws-sdk:dynamodb:putItem.waitForTaskToken']), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; const policy2 = resources.StateMachine2Role.Properties.Policies[0]; const policy3 = resources.StateMachine3Role.Properties.Policies[0]; const policy4 = resources.StateMachine4Role.Properties.Policies[0]; [policy1, policy2, policy3, policy4].forEach((policy) => { expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal([ 'dynamodb:UpdateItem', 'dynamodb:PutItem', 'dynamodb:GetItem', 'dynamodb:DeleteItem', 'dynamodb:UpdateTable', ]); }); expect(policy1.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTableArn]); expect(policy2.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTableArn]); expect(policy3.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTableArn]); expect(policy4.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTableArn]); }); 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 = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, { 'Fn::Join': ['/', ['table', externalHelloTable]] }], ], }; const externalWorldTable = { 'Fn::ImportValue': 'WorldStack:Table:Name' }; const worldTableArn = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, { 'Fn::Join': ['/', ['table', externalWorldTable]] }], ], }; const genStateMachine = (id, tableName, resources) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: resources[0], ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: resources[1], ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'C', }, C: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:getItem', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'D', }, D: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:deleteItem', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', externalHelloTable, ['arn:aws:states:::dynamodb:updateItem', 'arn:aws:states:::dynamodb:putItem']), myStateMachine2: genStateMachine('StateMachine2', externalWorldTable, ['arn:aws:states:::dynamodb:updateItem', 'arn:aws:states:::dynamodb:putItem']), myStateMachine3: genStateMachine('StateMachine3', externalHelloTable, ['arn:aws:states:::aws-sdk:dynamodb:updateItem', 'arn:aws:states:::aws-sdk:dynamodb:putItem']), myStateMachine4: genStateMachine('StateMachine4', externalWorldTable, ['arn:aws:states:::aws-sdk:dynamodb:updateItem.waitForTaskToken', 'arn:aws:states:::aws-sdk:dynamodb:putItem.waitForTaskToken']), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; const policy2 = resources.StateMachine2Role.Properties.Policies[0]; const policy3 = resources.StateMachine3Role.Properties.Policies[0]; const policy4 = resources.StateMachine4Role.Properties.Policies[0]; [policy1, policy2, policy3, policy4].forEach((policy) => { expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal([ 'dynamodb:UpdateItem', 'dynamodb:PutItem', 'dynamodb:GetItem', 'dynamodb:DeleteItem', ]); }); expect(policy1.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTableArn]); expect(policy2.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTableArn]); expect(policy3.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTableArn]); expect(policy4.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTableArn]); }); itParam('should give dynamodb permission to index table whenever IndexName is provided: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', ...getParamsOrArgs(queryLanguage, { TableName: tableName, IndexName: 'GSI1' }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal(['dynamodb:Query']); expect(policy.PolicyDocument.Statement[0].Resource[0]).to.be.deep.equal({ 'Fn::Join': [':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello']], }); expect(policy.PolicyDocument.Statement[0].Resource[1]).to.be.deep.equal({ 'Fn::Join': [':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello/index/GSI1']], }); }); itParam('should give dynamodb permission to * whenever TableName.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:updateItem', ...getParamsOrArgs(queryLanguage, { TableName: tableName }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:updateItem', ...getParamsOrArgs(queryLanguage, { 'TableName.$': '$.tableName' }, { TableName: '{% $tableName %}' }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal(['dynamodb:UpdateItem']); // even though some tasks target specific tables, because TableName.$ is used we // have to give broad permissions to allow execution to talk to whatever table // the input specifies expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); itParam('should give dynamodb permission to table/TableName/index/* when IndexName.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const genStateMachine = (id, tableName) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', ...getParamsOrArgs(queryLanguage, { TableName: tableName, 'IndexName.$': '$.myDynamicIndexName', }, { TableName: tableName, IndexName: '{% $myDynamicIndexName %}' }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal(['dynamodb:Query']); // even though some tasks target specific indices, because IndexName.$ is used we // have to give broad permissions to allow execution to talk to whatever index // the input specifies expect(policy.PolicyDocument.Statement[0].Resource[0]['Fn::Join'][1][5]).to.equal('table/hello/index/*'); }); itParam('should give dynamodb permission to table/* whenever TableName.$ and IndexName.$ are seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const genStateMachine = id => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:query', ...getParamsOrArgs(queryLanguage, { 'TableName.$': '$.myDynamicTableName', 'IndexName.$': '$.myDynamicIndexName', }, { TableName: '{% $myDynamicTableName %}', IndexName: '{% $myDynamicIndexName %}' }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1'), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal(['dynamodb:Query']); // even though some tasks target specific tables, because TableName.$ is used we // have to give broad permissions to allow execution to talk to whatever table // the input specifies expect(policy.PolicyDocument.Statement[0].Resource[0]).to.equal('*'); }); itParam('should give batch dynamodb permission for only tables referenced by state machine: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const helloTable = 'hello'; const helloTableArn = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/hello'], ], }; const worldTable = 'world'; const worldTableArn = { 'Fn::Join': [ ':', ['arn', { Ref: 'AWS::Partition' }, 'dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'table/world'], ], }; const genStateMachine = (id, tableName) => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', ...getParamsOrArgs(queryLanguage, { RequestItems: { [tableName]: [] } }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchGetItem', ...getParamsOrArgs(queryLanguage, { RequestItems: { [tableName]: {} } }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1', helloTable), myStateMachine2: genStateMachine('StateMachine2', worldTable), }, }; serverlessStepFunctions.compileIamRole(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const policy1 = resources.StateMachine1Role.Properties.Policies[0]; const policy2 = resources.StateMachine2Role.Properties.Policies[0]; [policy1, policy2].forEach((policy) => { expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal([ 'dynamodb:BatchWriteItem', 'dynamodb:BatchGetItem', ]); }); expect(policy1.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([helloTableArn]); expect(policy2.PolicyDocument.Statement[0].Resource) .to.be.deep.equal([worldTableArn]); }); itParam('should give batch dynamodb permission to * whenever RequestItems.$ is seen: ${value}', ['JSONPath', 'JSONata'], (queryLanguage) => { const genStateMachine = id => ({ id, definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', ...getParamsOrArgs(queryLanguage, { RequestItems: { tableName: [] } }), Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:dynamodb:batchWriteItem', ...getParamsOrArgs(queryLanguage, { 'RequestItems.$': '$.requestItems' }, { RequestItems: '{% $requestItems %}' }), End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('StateMachine1'), }, }; serverlessStepFunctions.compileIamRole(); const policy = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0]; expect(policy.PolicyDocument.Statement[0].Action) .to.be.deep.equal(['dynamodb:BatchWriteItem']); // even though some tasks target specific tables, because RequestItems.$ is used we // have to give broad permissions to allow execution to talk to whatever table // the input specifies expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*'); }); it('should give Redshift Data permissions for safe actions', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:listStatements', Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:describeStatement', Next: 'C', }, C: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:getStatementResult', Next: 'D', }, D: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:cancelStatement', End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ListStatements'); expect(statement.Action).to.include('redshift-data:DescribeStatement'); expect(statement.Action).to.include('redshift-data:GetStatementResult'); expect(statement.Action).to.include('redshift-data:CancelStatement'); expect(statement.Resource).to.equal('*'); }); it('should give Redshift Data permissions for unsafe actions on a workgroup, given its ARN', () => { const workgroupName = 'arn:aws:redshift-serverless:us-east-1:012345678901:workgroup/01234567-89ab-cdef-0123-456789abcdef'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { WorkgroupName: workgroupName, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { WorkgroupName: workgroupName, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ExecuteStatement'); expect(statement.Action).to.include('redshift-data:BatchExecuteStatement'); expect(statement.Resource).to.have.deep.members([ workgroupName, ]); }); it('should give Redshift Data permissions for unsafe actions on all workgroups, given a name', () => { const workgroupName = 'myWorkgroup'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { WorkgroupName: workgroupName, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { WorkgroupName: workgroupName, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ExecuteStatement'); expect(statement.Action).to.include('redshift-data:BatchExecuteStatement'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': 'arn:${AWS::Partition}:redshift-serverless:${AWS::Region}:${AWS::AccountId}:workgroup/*', }]); }); it('should give Redshift Data permissions for unsafe actions on all workgroups, promised a name', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { 'WorkgroupName.$': '$', }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { 'WorkgroupName.$': '$', }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ExecuteStatement'); expect(statement.Action).to.include('redshift-data:BatchExecuteStatement'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': 'arn:${AWS::Partition}:redshift-serverless:${AWS::Region}:${AWS::AccountId}:workgroup/*', }]); }); it('should give Redshift Data permissions for unsafe actions on a cluster, given its identifier', () => { const clusterIdentifier = 'myCluster'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { ClusterIdentifier: clusterIdentifier, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { ClusterIdentifier: clusterIdentifier, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ExecuteStatement'); expect(statement.Action).to.include('redshift-data:BatchExecuteStatement'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:cluster:${clusterIdentifier}`, }]); }); it('should give Redshift Data permissions for unsafe actions on all clusters, promised an identifier', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { 'ClusterIdentifier.$': '$', }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { 'ClusterIdentifier.$': '$', }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[0]; expect(statement.Action).to.include('redshift-data:ExecuteStatement'); expect(statement.Action).to.include('redshift-data:BatchExecuteStatement'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:cluster:*', }]); }); it('should give permissions for unsafe Redshift Data actions to get the value of a secret, given its ARN', () => { const secretArn = 'arn:aws:secretsmanager:us-east-1:012345678901:secret:mySecret-ABab01'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { SecretArn: secretArn, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { SecretArn: secretArn, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[1]; expect(statement.Action).to.include('secretsmanager:GetSecretValue'); expect(statement.Resource).to.have.deep.members([ secretArn, ]); }); it('should give permissions for unsafe Redshift Data actions to get the values of some secrets, given a name', () => { const secretArn = 'mySecret'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { SecretArn: secretArn, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { SecretArn: secretArn, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[1]; expect(statement.Action).to.include('secretsmanager:GetSecretValue'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:mySecret*', }]); }); it('should give permissions for unsafe Redshift Data actions to get the values of all secrets, promised an ARN', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { 'SecretArn.$': '$', }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { 'SecretArn.$': '$', }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[1]; expect(statement.Action).to.include('secretsmanager:GetSecretValue'); expect(statement.Resource).to.have.deep.members([{ 'Fn::Sub': 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:*', }]); }); it('should give permissions for unsafe Redshift Data actions to get temporary credentials for a database user, given its name', () => { const dbUser = 'myUser'; const clusterIdentifier = 'myCluster'; const database = 'myDatabase'; serverless.service.stepFunctions = { stateMachines: { myStateMachine: { id: 'StateMachine1', definition: { StartAt: 'A', States: { A: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement', Parameters: { DbUser: dbUser, ClusterIdentifier: clusterIdentifier, Database: database, }, Next: 'B', }, B: { Type: 'Task', Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement', Parameters: { DbUser: dbUser, ClusterIdentifier: clusterIdentifier, Database: database, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileIamRole(); const statement = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources.StateMachine1Role .Properties.Policies[0].PolicyDocument.Statement[1]; expect(statement.Action).to.include('