serverless-step-functions
Version:
The module is AWS Step Functions plugin for Serverless Framework
1,401 lines (1,292 loc) • 138 kB
JavaScript
'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('