Skip to content

Commit fc6de3d

Browse files
committed
feat: add support for Redshift Data
1 parent fe688f3 commit fc6de3d

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,28 @@ function getDynamoDBPermissions(action, state) {
192192
}];
193193
}
194194

195+
function getRedshiftDataPermissions(action, state) {
196+
if (['redshift-data:ExecuteStatement', 'redshift-data:BatchExecuteStatement'].includes(action)) {
197+
const clusterName = _.has(state, 'Parameters.ClusterIdentifier') ? state.Parameters.ClusterIdentifier : '*';
198+
const dbName = _.has(state, 'Parameters.Database') ? state.Parameters.Database : '*';
199+
const dbUser = _.has(state, 'Parameters.DbUser') ? state.Parameters.DbUser : '*';
200+
return [{
201+
action,
202+
resource: { 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:cluster:${clusterName}` },
203+
}, {
204+
action: 'redshift:GetClusterCredentials',
205+
resource: [
206+
{ 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbname:${clusterName}/${dbName}` },
207+
{ 'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbuser:${clusterName}/${dbUser}` },
208+
],
209+
}];
210+
}
211+
return [{
212+
action,
213+
resource: '*',
214+
}];
215+
}
216+
195217
function getLambdaPermissions(state) {
196218
// function name can be name-only, name-only with alias, full arn or partial arn
197219
// https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestParameters
@@ -445,6 +467,19 @@ function getIamPermissions(taskStates) {
445467
case 'arn:aws:states:::aws-sdk:dynamodb:updateTable':
446468
return getDynamoDBPermissions('dynamodb:UpdateTable', state);
447469

470+
case 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement':
471+
return getRedshiftDataPermissions('redshift-data:ExecuteStatement', state);
472+
case 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement':
473+
return getRedshiftDataPermissions('redshift-data:BatchExecuteStatement', state);
474+
case 'arn:aws:states:::aws-sdk:redshiftdata:listStatements':
475+
return getRedshiftDataPermissions('redshift-data:ListStatements', state);
476+
case 'arn:aws:states:::aws-sdk:redshiftdata:describeStatement':
477+
return getRedshiftDataPermissions('redshift-data:DescribeStatement', state);
478+
case 'arn:aws:states:::aws-sdk:redshiftdata:getStatementResult':
479+
return getRedshiftDataPermissions('redshift-data:GetStatementResult', state);
480+
case 'arn:aws:states:::aws-sdk:redshiftdata:cancelStatement':
481+
return getRedshiftDataPermissions('redshift-data:CancelStatement', state);
482+
448483
case 'arn:aws:states:::batch:submitJob.sync':
449484
case 'arn:aws:states:::batch:submitJob':
450485
return getBatchPermissions();

lib/deploy/stepFunctions/compileIamRole.test.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,244 @@ describe('#compileIamRole', () => {
778778
expect(policy.PolicyDocument.Statement[0].Resource).to.equal('*');
779779
});
780780

781+
it('should give Redshift Data permissions to * for safe actions', () => {
782+
serverless.service.stepFunctions = {
783+
stateMachines: {
784+
myStateMachine: {
785+
id: 'StateMachine1',
786+
definition: {
787+
StartAt: 'A',
788+
States: {
789+
A: {
790+
Type: 'Task',
791+
Next: 'B',
792+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:listStatements',
793+
},
794+
B: {
795+
Type: 'Task',
796+
Next: 'C',
797+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:describeStatement',
798+
},
799+
C: {
800+
Type: 'Task',
801+
Next: 'D',
802+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:getStatementResult',
803+
},
804+
D: {
805+
Type: 'Task',
806+
End: true,
807+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:cancelStatement',
808+
},
809+
},
810+
},
811+
},
812+
},
813+
};
814+
serverlessStepFunctions.compileIamRole();
815+
const statement = serverlessStepFunctions.serverless.service.provider
816+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
817+
.PolicyDocument.Statement[0];
818+
expect(statement.Action).to.include('redshift-data:ListStatements');
819+
expect(statement.Action).to.include('redshift-data:DescribeStatement');
820+
expect(statement.Action).to.include('redshift-data:GetStatementResult');
821+
expect(statement.Action).to.include('redshift-data:CancelStatement');
822+
expect(statement.Resource).to.equal('*');
823+
});
824+
825+
it('should give Redshift Data permissions to clusters for unsafe actions', () => {
826+
serverless.service.stepFunctions = {
827+
stateMachines: {
828+
myStateMachine: {
829+
id: 'StateMachine1',
830+
definition: {
831+
StartAt: 'A',
832+
States: {
833+
A: {
834+
Type: 'Task',
835+
Next: 'B',
836+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
837+
},
838+
B: {
839+
Type: 'Task',
840+
End: true,
841+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
842+
},
843+
},
844+
},
845+
},
846+
},
847+
};
848+
serverlessStepFunctions.compileIamRole();
849+
const statement = serverlessStepFunctions.serverless.service.provider
850+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
851+
.PolicyDocument.Statement[0];
852+
expect(statement.Action).to.include('redshift-data:ExecuteStatement');
853+
expect(statement.Action).to.include('redshift-data:BatchExecuteStatement');
854+
expect(statement.Resource).to.have.deep.members([{
855+
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:cluster:*',
856+
}]);
857+
});
858+
859+
it('should give Redshift Data permissions to a specified cluster for unsafe actions', () => {
860+
const clusterName = 'myCluster';
861+
serverless.service.stepFunctions = {
862+
stateMachines: {
863+
myStateMachine: {
864+
id: 'StateMachine1',
865+
definition: {
866+
StartAt: 'A',
867+
States: {
868+
A: {
869+
Type: 'Task',
870+
Next: 'B',
871+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
872+
Parameters: {
873+
ClusterIdentifier: clusterName,
874+
},
875+
},
876+
B: {
877+
Type: 'Task',
878+
End: true,
879+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
880+
Parameters: {
881+
ClusterIdentifier: clusterName,
882+
},
883+
},
884+
},
885+
},
886+
},
887+
},
888+
};
889+
serverlessStepFunctions.compileIamRole();
890+
const statement = serverlessStepFunctions.serverless.service.provider
891+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
892+
.PolicyDocument.Statement[0];
893+
expect(statement.Action).to.include('redshift-data:ExecuteStatement');
894+
expect(statement.Action).to.include('redshift-data:BatchExecuteStatement');
895+
expect(statement.Resource).to.have.deep.members([{
896+
'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:cluster:${clusterName}`,
897+
}]);
898+
});
899+
900+
it('should give redshift:GetClusterCredentials permission to databases and database users for unsafe actions', () => {
901+
serverless.service.stepFunctions = {
902+
stateMachines: {
903+
myStateMachine: {
904+
id: 'StateMachine1',
905+
definition: {
906+
StartAt: 'A',
907+
States: {
908+
A: {
909+
Type: 'Task',
910+
Next: 'B',
911+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
912+
},
913+
B: {
914+
Type: 'Task',
915+
End: true,
916+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
917+
},
918+
},
919+
},
920+
},
921+
},
922+
};
923+
serverlessStepFunctions.compileIamRole();
924+
const statement = serverlessStepFunctions.serverless.service.provider
925+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
926+
.PolicyDocument.Statement[1];
927+
expect(statement.Action).to.include('redshift:GetClusterCredentials');
928+
expect(statement.Resource).to.have.deep.members([{
929+
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbname:*/*',
930+
}, {
931+
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:*/*',
932+
}]);
933+
});
934+
935+
it('should give redshift:GetClusterCredentials permission to specified databases for unsafe actions', () => {
936+
const dbName = 'myDatabase';
937+
serverless.service.stepFunctions = {
938+
stateMachines: {
939+
myStateMachine: {
940+
id: 'StateMachine1',
941+
definition: {
942+
StartAt: 'A',
943+
States: {
944+
A: {
945+
Type: 'Task',
946+
Next: 'B',
947+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
948+
Parameters: {
949+
Database: dbName,
950+
},
951+
},
952+
B: {
953+
Type: 'Task',
954+
End: true,
955+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
956+
Parameters: {
957+
Database: dbName,
958+
},
959+
},
960+
},
961+
},
962+
},
963+
},
964+
};
965+
serverlessStepFunctions.compileIamRole();
966+
const statement = serverlessStepFunctions.serverless.service.provider
967+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
968+
.PolicyDocument.Statement[1];
969+
expect(statement.Action).to.include('redshift:GetClusterCredentials');
970+
expect(statement.Resource).to.have.deep.members([{
971+
'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbname:*/${dbName}`,
972+
}, {
973+
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:*/*',
974+
}]);
975+
});
976+
977+
it('should give redshift:GetClusterCredentials permission to specified database users for unsafe actions', () => {
978+
const dbUser = 'myDatabaseUser';
979+
serverless.service.stepFunctions = {
980+
stateMachines: {
981+
myStateMachine: {
982+
id: 'StateMachine1',
983+
definition: {
984+
StartAt: 'A',
985+
States: {
986+
A: {
987+
Type: 'Task',
988+
Next: 'B',
989+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:executeStatement',
990+
Parameters: {
991+
DbUser: dbUser,
992+
},
993+
},
994+
B: {
995+
Type: 'Task',
996+
End: true,
997+
Resource: 'arn:aws:states:::aws-sdk:redshiftdata:batchExecuteStatement',
998+
Parameters: {
999+
DbUser: dbUser,
1000+
},
1001+
},
1002+
},
1003+
},
1004+
},
1005+
},
1006+
};
1007+
serverlessStepFunctions.compileIamRole();
1008+
const statement = serverlessStepFunctions.serverless.service.provider
1009+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
1010+
.PolicyDocument.Statement[1];
1011+
expect(statement.Action).to.include('redshift:GetClusterCredentials');
1012+
expect(statement.Resource).to.have.deep.members([{
1013+
'Fn::Sub': 'arn:${AWS::Partition}:redshift:${AWS::Region}:${AWS::AccountId}:dbname:*/*',
1014+
}, {
1015+
'Fn::Sub': `arn:\${AWS::Partition}:redshift:\${AWS::Region}:\${AWS::AccountId}:dbuser:*/${dbUser}`,
1016+
}]);
1017+
});
1018+
7811019
it('should give batch permissions (too permissive, but mirrors console behaviour)', () => {
7821020
const genStateMachine = id => ({
7831021
id,

0 commit comments

Comments
 (0)