Skip to content

Commit

Permalink
feat: add support for Redshift Data
Browse files Browse the repository at this point in the history
  • Loading branch information
kmfukuda committed Sep 17, 2022
1 parent fe688f3 commit fc6de3d
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 0 deletions.
35 changes: 35 additions & 0 deletions lib/deploy/stepFunctions/compileIamRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,28 @@ function getDynamoDBPermissions(action, state) {
}];
}

function getRedshiftDataPermissions(action, state) {
if (['redshift-data:ExecuteStatement', 'redshift-data:BatchExecuteStatement'].includes(action)) {
const clusterName = _.has(state, 'Parameters.ClusterIdentifier') ? state.Parameters.ClusterIdentifier : '*';
const dbName = _.has(state, 'Parameters.Database') ? state.Parameters.Database : '*';
const dbUser = _.has(state, 'Parameters.DbUser') ? state.Parameters.DbUser : '*';
return [{
action,
resource: { 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:cluster:${clusterName}` },
}, {
action: 'redshift:GetClusterCredentials',
resource: [
{ 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbname:${clusterName}/${dbName}` },
{ 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbuser:${clusterName}/${dbUser}` },
],
}];
}
return [{
action,
resource: '*',
}];
}

function getLambdaPermissions(state) {
// function name can be name-only, name-only with alias, full arn or partial arn
// https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestParameters
Expand Down Expand Up @@ -445,6 +467,19 @@ function getIamPermissions(taskStates) {
case 'arn:aws:states:::aws-sdk:dynamodb:updateTable':
return getDynamoDBPermissions('dynamodb:UpdateTable', state);

case 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement':
return getRedshiftDataPermissions('redshift-data:ExecuteStatement', state);
case 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement':
return getRedshiftDataPermissions('redshift-data:BatchExecuteStatement', state);
case 'arn:aws:states:::aws-sdk:redshiftdata:listStatements':
return getRedshiftDataPermissions('redshift-data:ListStatements', state);
case 'arn:aws:states:::aws-sdk:redshiftdata:describeStatement':
return getRedshiftDataPermissions('redshift-data:DescribeStatement', state);
case 'arn:aws:states:::aws-sdk:redshiftdata:getStatementResult':
return getRedshiftDataPermissions('redshift-data:GetStatementResult', state);
case 'arn:aws:states:::aws-sdk:redshiftdata:cancelStatement':
return getRedshiftDataPermissions('redshift-data:CancelStatement', state);

case 'arn:aws:states:::batch:submitJob.sync':
case 'arn:aws:states:::batch:submitJob':
return getBatchPermissions();
Expand Down
238 changes: 238 additions & 0 deletions lib/deploy/stepFunctions/compileIamRole.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,244 @@ describe('#compileIamRole', () => {
expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*');
});

it('should give Redshift Data permissions to * for safe actions', () => {
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:listStatements',
},
B: {
Type: 'Task',
Next: 'C',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:describeStatement',
},
C: {
Type: 'Task',
Next: 'D',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:getStatementResult',
},
D: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:cancelStatement',
},
},
},
},
},
};
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 to clusters for unsafe actions', () => {
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
},
B: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
},
},
},
},
},
};
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 Redshift Data permissions to a specified cluster for unsafe actions', () => {
const clusterName = 'myCluster';
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
Parameters: {
ClusterIdentifier: clusterName,
},
},
B: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
Parameters: {
ClusterIdentifier: clusterName,
},
},
},
},
},
},
};
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:${clusterName}`,
}]);
});

it('should give redshift:GetClusterCredentials permission to databases and database users for unsafe actions', () => {
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
},
B: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
},
},
},
},
},
};
serverlessStepFunctions.compileIamRole();
const statement = serverlessStepFunctions.serverless.service.provider
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
.PolicyDocument.Statement[1];
expect(statement.Action).to.include('redshift:GetClusterCredentials');
expect(statement.Resource).to.have.deep.members([{
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbname:*/*',
}, {
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:*/*',
}]);
});

it('should give redshift:GetClusterCredentials permission to specified databases for unsafe actions', () => {
const dbName = 'myDatabase';
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
Parameters: {
Database: dbName,
},
},
B: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
Parameters: {
Database: dbName,
},
},
},
},
},
},
};
serverlessStepFunctions.compileIamRole();
const statement = serverlessStepFunctions.serverless.service.provider
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
.PolicyDocument.Statement[1];
expect(statement.Action).to.include('redshift:GetClusterCredentials');
expect(statement.Resource).to.have.deep.members([{
'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbname:*/${dbName}`,
}, {
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:*/*',
}]);
});

it('should give redshift:GetClusterCredentials permission to specified database users for unsafe actions', () => {
const dbUser = 'myDatabaseUser';
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine: {
id: 'StateMachine1',
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Next: 'B',
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
Parameters: {
DbUser: dbUser,
},
},
B: {
Type: 'Task',
End: true,
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
Parameters: {
DbUser: dbUser,
},
},
},
},
},
},
};
serverlessStepFunctions.compileIamRole();
const statement = serverlessStepFunctions.serverless.service.provider
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
.PolicyDocument.Statement[1];
expect(statement.Action).to.include('redshift:GetClusterCredentials');
expect(statement.Resource).to.have.deep.members([{
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbname:*/*',
}, {
'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbuser:*/${dbUser}`,
}]);
});

it('should give batch permissions (too permissive, but mirrors console behaviour)', () => {
const genStateMachine = id => ({
id,
Expand Down

0 comments on commit fc6de3d

Please sign in to comment.