Skip to content

Commit ff502a9

Browse files
authored
feat: optional redshift federation role (#6)
* feat: add optional redshift federation iam role * fix: test and spec config * feat: role name for federation redshift role
1 parent e73e83d commit ff502a9

3 files changed

Lines changed: 363 additions & 0 deletions

File tree

redshift.cfndsl.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,42 @@
7272

7373
iam_role_arns << FnGetAtt(:RedshiftIAMRole, "Arn")
7474

75+
redshift_federation_iam_role = external_parameters.fetch(:redshift_federation_iam_role, {})
76+
if !redshift_federation_iam_role.empty? && redshift_federation_iam_role["enable"]
77+
samlProviderName = redshift_federation_iam_role["assume_role_policy"]["principal"]["providerName"]
78+
samlAud = redshift_federation_iam_role["assume_role_policy"]["condition"]["samlAud"]
79+
assumeRolePolicy = {
80+
"Version": "2012-10-17",
81+
"Statement": [
82+
{
83+
"Effect": "Allow",
84+
"Principal": {
85+
"Federated": FnSub("arn:aws:iam::${AWS::AccountId}:saml-provider/#{samlProviderName}")
86+
},
87+
"Action": [
88+
"sts:AssumeRoleWithSAML",
89+
"sts:TagSession"
90+
],
91+
"Condition": {
92+
"StringEquals": {
93+
"SAML:aud": "#{samlAud}"
94+
}
95+
}
96+
}
97+
]
98+
}
99+
IAM_Role(:RedshiftFederationIAMRole) {
100+
RoleName "redshift-federation-role"
101+
AssumeRolePolicyDocument assumeRolePolicy
102+
Policies iam_role_policies(iam_policies['redshift-federation'])
103+
}
104+
105+
Output("RedshiftFederationIamRoleArn") {
106+
Value FnGetAtt(:RedshiftFederationIAMRole, "Arn")
107+
Export FnSub("${EnvironmentName}-#{external_parameters[:component_name]}-redshift-federation-iam-role-arn")
108+
}
109+
end
110+
75111
external_parameters.fetch(:additional_iam_roles, {}).each do |k,v|
76112
IAM_Role(k) {
77113
AssumeRolePolicyDocument service_assume_role_policy(['redshift','glue'])
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
require 'yaml'
2+
3+
describe 'compiled component redshift' do
4+
5+
context 'cftest' do
6+
it 'compiles test' do
7+
expect(system("cfhighlander cftest #{@validate} --tests tests/redshift_federation_iam_role.test.yaml")).to be_truthy
8+
end
9+
end
10+
11+
let(:template) { YAML.load_file("#{File.dirname(__FILE__)}/../out/tests/redshift_federation_iam_role/redshift.compiled.yaml") }
12+
13+
context "Resource" do
14+
15+
16+
context "RedshiftLoggingS3Bucket" do
17+
let(:resource) { template["Resources"]["RedshiftLoggingS3Bucket"] }
18+
19+
it "is of type AWS::S3::Bucket" do
20+
expect(resource["Type"]).to eq("AWS::S3::Bucket")
21+
end
22+
23+
it "to have property BucketName" do
24+
expect(resource["Properties"]["BucketName"]).to eq({"Fn::Join"=>["-", ["redshift", "logs", {"Ref"=>"EnvironmentName"}, {"Ref"=>"AWS::Region"}, {"Fn::Select"=>[4, {"Fn::Split"=>["-", {"Fn::Select"=>[2, {"Fn::Split"=>["/", {"Ref"=>"AWS::StackId"}]}]}]}]}]]})
25+
end
26+
27+
it "to have property BucketEncryption" do
28+
expect(resource["Properties"]["BucketEncryption"]).to eq({"ServerSideEncryptionConfiguration"=>[{"ServerSideEncryptionByDefault"=>{"SSEAlgorithm"=>"AES256"}}]})
29+
end
30+
31+
it "to have property PublicAccessBlockConfiguration" do
32+
expect(resource["Properties"]["PublicAccessBlockConfiguration"]).to eq({"BlockPublicAcls"=>true, "BlockPublicPolicy"=>true, "IgnorePublicAcls"=>true, "RestrictPublicBuckets"=>true})
33+
end
34+
35+
it "to have property Tags" do
36+
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}])
37+
end
38+
39+
end
40+
41+
context "RedshiftLoggingS3BucketPolicy" do
42+
let(:resource) { template["Resources"]["RedshiftLoggingS3BucketPolicy"] }
43+
44+
it "is of type AWS::S3::BucketPolicy" do
45+
expect(resource["Type"]).to eq("AWS::S3::BucketPolicy")
46+
end
47+
48+
it "to have property Bucket" do
49+
expect(resource["Properties"]["Bucket"]).to eq({"Ref"=>"RedshiftLoggingS3Bucket"})
50+
end
51+
52+
it "to have property PolicyDocument" do
53+
expect(resource["Properties"]["PolicyDocument"]).to eq({"Statement"=>[{"Principal"=>{"Service"=>"redshift.amazonaws.com"}, "Action"=>["s3:PutObject", "s3:GetBucketAcl"], "Effect"=>"Allow", "Resource"=>[{"Fn::Sub"=>"arn:aws:s3:::${RedshiftLoggingS3Bucket}"}, {"Fn::Sub"=>"arn:aws:s3:::${RedshiftLoggingS3Bucket}/*"}]}]})
54+
end
55+
56+
end
57+
58+
context "RedshiftIAMRole" do
59+
let(:resource) { template["Resources"]["RedshiftIAMRole"] }
60+
61+
it "is of type AWS::IAM::Role" do
62+
expect(resource["Type"]).to eq("AWS::IAM::Role")
63+
end
64+
65+
it "to have property AssumeRolePolicyDocument" do
66+
expect(resource["Properties"]["AssumeRolePolicyDocument"]).to eq({"Version"=>"2012-10-17", "Statement"=>[{"Effect"=>"Allow", "Principal"=>{"Service"=>"redshift.amazonaws.com"}, "Action"=>"sts:AssumeRole"}, {"Effect"=>"Allow", "Principal"=>{"Service"=>"glue.amazonaws.com"}, "Action"=>"sts:AssumeRole"}]})
67+
end
68+
69+
it "to have property Policies" do
70+
expect(resource["Properties"]["Policies"]).to eq([{"PolicyName"=>"s3-logging", "PolicyDocument"=>{"Statement"=>[{"Sid"=>"s3logging", "Action"=>["s3:GetBucketLocation", "s3:GetObject", "s3:ListMultipartUploadParts", "s3:ListBucket", "s3:ListBucketMultipartUploads"], "Resource"=>[{"Fn::Sub"=>"arn:aws:s3:::${RedshiftLoggingS3Bucket}"}, {"Fn::Sub"=>"arn:aws:s3:::${RedshiftLoggingS3Bucket}/*"}], "Effect"=>"Allow"}]}}, {"PolicyName"=>"glue", "PolicyDocument"=>{"Statement"=>[{"Sid"=>"glue", "Action"=>["glue:CreateDatabase", "glue:DeleteDatabase", "glue:GetDatabase", "glue:GetDatabases", "glue:UpdateDatabase", "glue:CreateTable", "glue:DeleteTable", "glue:BatchDeleteTable", "glue:UpdateTable", "glue:GetTable", "glue:GetTables", "glue:BatchCreatePartition", "glue:CreatePartition", "glue:DeletePartition", "glue:BatchDeletePartition", "glue:UpdatePartition", "glue:GetPartition", "glue:GetPartitions", "glue:BatchGetPartition"], "Resource"=>["*"], "Effect"=>"Allow"}]}}, {"PolicyName"=>"logs", "PolicyDocument"=>{"Statement"=>[{"Sid"=>"logs", "Action"=>["logs:*"], "Resource"=>["*"], "Effect"=>"Allow"}]}}])
71+
end
72+
73+
end
74+
75+
context "RedshiftFederationIAMRole" do
76+
let(:resource) { template["Resources"]["RedshiftFederationIAMRole"] }
77+
78+
it "is of type AWS::IAM::Role" do
79+
expect(resource["Type"]).to eq("AWS::IAM::Role")
80+
end
81+
82+
it "to have property AssumeRolePolicyDocument" do
83+
expect(resource["Properties"]["AssumeRolePolicyDocument"]).to eq({"Version"=>"2012-10-17", "Statement"=>[{"Effect"=>"Allow", "Principal"=>{"Federated"=>{"Fn::Sub"=>"arn:aws:iam::${AWS::AccountId}:saml-provider/redshift-federation-saml-provider"}}, "Action"=>["sts:AssumeRoleWithSAML", "sts:TagSession"], "Condition"=>{"StringEquals"=>{"SAML:aud"=>"http://localhost:7890/redshift/"}}}]})
84+
end
85+
86+
it "to have property Policies" do
87+
expect(resource["Properties"]["Policies"]).to eq([{"PolicyName"=>"redshift", "PolicyDocument"=>{"Statement"=>[{"Sid"=>"redshift", "Action"=>["redshift:CreateClusterUser", "redshift:JoinGroup", "redshift:GetClusterCredentials", "redshift:ListSchemas", "redshift:ListTables", "redshift:ListDatabases", "redshift:ExecuteQuery", "redshift:FetchResults", "redshift:CancelQuery", "redshift:DescribeClusters", "redshift:DescribeQuery", "redshift:DescribeTable"], "Resource"=>[{"Fn::Sub"=>"arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:cluster:${RedshiftCluster}"}, {"Fn::Sub"=>"arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:${RedshiftCluster}/${!redshift:DbUser}"}, {"Fn::Sub"=>"arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbname:${RedshiftCluster}/${!redshift:DbName}"}, {"Fn::Sub"=>"arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbgroup:${RedshiftCluster}/mygroup"}], "Effect"=>"Allow"}]}}])
88+
end
89+
90+
end
91+
92+
context "RedshiftSecurityGroup" do
93+
let(:resource) { template["Resources"]["RedshiftSecurityGroup"] }
94+
95+
it "is of type AWS::EC2::SecurityGroup" do
96+
expect(resource["Type"]).to eq("AWS::EC2::SecurityGroup")
97+
end
98+
99+
it "to have property GroupDescription" do
100+
expect(resource["Properties"]["GroupDescription"]).to eq({"Fn::Sub"=>"${EnvironmentName} - Redshift cluster security group"})
101+
end
102+
103+
it "to have property VpcId" do
104+
expect(resource["Properties"]["VpcId"]).to eq({"Ref"=>"VpcId"})
105+
end
106+
107+
it "to have property Tags" do
108+
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}])
109+
end
110+
111+
end
112+
113+
context "SecretRedshiftMasterUser" do
114+
let(:resource) { template["Resources"]["SecretRedshiftMasterUser"] }
115+
116+
it "is of type AWS::SecretsManager::Secret" do
117+
expect(resource["Type"]).to eq("AWS::SecretsManager::Secret")
118+
end
119+
120+
it "to have property Description" do
121+
expect(resource["Properties"]["Description"]).to eq({"Fn::Sub"=>"${EnvironmentName} Secrets Manager to store Redshift user credentials"})
122+
end
123+
124+
it "to have property Name" do
125+
expect(resource["Properties"]["Name"]).to eq("SecretRedshiftMasterUser")
126+
end
127+
128+
it "to have property GenerateSecretString" do
129+
expect(resource["Properties"]["GenerateSecretString"]).to eq({"SecretStringTemplate"=>{"Fn::Sub"=>"{\"username\": \"${MasterUsername}\"}"}, "GenerateStringKey"=>"password", "PasswordLength"=>32, "ExcludePunctuation"=>true})
130+
end
131+
132+
end
133+
134+
context "SecretTargetAttachment" do
135+
let(:resource) { template["Resources"]["SecretTargetAttachment"] }
136+
137+
it "is of type AWS::SecretsManager::SecretTargetAttachment" do
138+
expect(resource["Type"]).to eq("AWS::SecretsManager::SecretTargetAttachment")
139+
end
140+
141+
it "to have property SecretId" do
142+
expect(resource["Properties"]["SecretId"]).to eq({"Ref"=>"SecretRedshiftMasterUser"})
143+
end
144+
145+
it "to have property TargetId" do
146+
expect(resource["Properties"]["TargetId"]).to eq({"Ref"=>"RedshiftCluster"})
147+
end
148+
149+
it "to have property TargetType" do
150+
expect(resource["Properties"]["TargetType"]).to eq("AWS::Redshift::Cluster")
151+
end
152+
153+
end
154+
155+
context "RedshiftClusterSubnetGroup" do
156+
let(:resource) { template["Resources"]["RedshiftClusterSubnetGroup"] }
157+
158+
it "is of type AWS::Redshift::ClusterSubnetGroup" do
159+
expect(resource["Type"]).to eq("AWS::Redshift::ClusterSubnetGroup")
160+
end
161+
162+
it "to have property Description" do
163+
expect(resource["Properties"]["Description"]).to eq({"Fn::Sub"=>"${EnvironmentName} - Redshift cluster subnet group"})
164+
end
165+
166+
it "to have property SubnetIds" do
167+
expect(resource["Properties"]["SubnetIds"]).to eq({"Ref"=>"SubnetIds"})
168+
end
169+
170+
it "to have property Tags" do
171+
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}])
172+
end
173+
174+
end
175+
176+
context "RedshiftClusterParameterGroup" do
177+
let(:resource) { template["Resources"]["RedshiftClusterParameterGroup"] }
178+
179+
it "is of type AWS::Redshift::ClusterParameterGroup" do
180+
expect(resource["Type"]).to eq("AWS::Redshift::ClusterParameterGroup")
181+
end
182+
183+
it "to have property Description" do
184+
expect(resource["Properties"]["Description"]).to eq({"Fn::Sub"=>"${EnvironmentName} - Redshift cluster parameter group"})
185+
end
186+
187+
it "to have property ParameterGroupFamily" do
188+
expect(resource["Properties"]["ParameterGroupFamily"]).to eq("redshift-1.0")
189+
end
190+
191+
it "to have property Parameters" do
192+
expect(resource["Properties"]["Parameters"]).to eq([{"ParameterName"=>"enable_user_activity_logging", "ParameterValue"=>"true"}])
193+
end
194+
195+
it "to have property Tags" do
196+
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}])
197+
end
198+
199+
end
200+
201+
context "RedshiftCluster" do
202+
let(:resource) { template["Resources"]["RedshiftCluster"] }
203+
204+
it "is of type AWS::Redshift::Cluster" do
205+
expect(resource["Type"]).to eq("AWS::Redshift::Cluster")
206+
end
207+
208+
it "to have property ClusterType" do
209+
expect(resource["Properties"]["ClusterType"]).to eq({"Fn::If"=>["RedshiftSingleNodeClusterCondition", "single-node", "multi-node"]})
210+
end
211+
212+
it "to have property NumberOfNodes" do
213+
expect(resource["Properties"]["NumberOfNodes"]).to eq({"Fn::If"=>["RedshiftSingleNodeClusterCondition", {"Ref"=>"AWS::NoValue"}, {"Ref"=>"NumberOfNodes"}]})
214+
end
215+
216+
it "to have property Encrypted" do
217+
expect(resource["Properties"]["Encrypted"]).to eq({"Ref"=>"Encrypt"})
218+
end
219+
220+
it "to have property KmsKeyId" do
221+
expect(resource["Properties"]["KmsKeyId"]).to eq({"Fn::If"=>["EncryptWithKMS", {"Ref"=>"KmsKeyId"}, {"Ref"=>"AWS::NoValue"}]})
222+
end
223+
224+
it "to have property NodeType" do
225+
expect(resource["Properties"]["NodeType"]).to eq({"Ref"=>"NodeType"})
226+
end
227+
228+
it "to have property DBName" do
229+
expect(resource["Properties"]["DBName"]).to eq({"Fn::If"=>["DatabaseNameSet", {"Ref"=>"DatabaseName"}, {"Ref"=>"AWS::NoValue"}]})
230+
end
231+
232+
it "to have property PubliclyAccessible" do
233+
expect(resource["Properties"]["PubliclyAccessible"]).to eq(false)
234+
end
235+
236+
it "to have property MasterUsername" do
237+
expect(resource["Properties"]["MasterUsername"]).to eq({"Fn::Sub"=>"{{resolve:secretsmanager:${SecretRedshiftMasterUser}:SecretString:username}}"})
238+
end
239+
240+
it "to have property MasterUserPassword" do
241+
expect(resource["Properties"]["MasterUserPassword"]).to eq({"Fn::Sub"=>"{{resolve:secretsmanager:${SecretRedshiftMasterUser}:SecretString:password}}"})
242+
end
243+
244+
it "to have property ClusterParameterGroupName" do
245+
expect(resource["Properties"]["ClusterParameterGroupName"]).to eq({"Ref"=>"RedshiftClusterParameterGroup"})
246+
end
247+
248+
it "to have property ClusterSubnetGroupName" do
249+
expect(resource["Properties"]["ClusterSubnetGroupName"]).to eq({"Ref"=>"RedshiftClusterSubnetGroup"})
250+
end
251+
252+
it "to have property VpcSecurityGroupIds" do
253+
expect(resource["Properties"]["VpcSecurityGroupIds"]).to eq([{"Ref"=>"RedshiftSecurityGroup"}])
254+
end
255+
256+
it "to have property AutomatedSnapshotRetentionPeriod" do
257+
expect(resource["Properties"]["AutomatedSnapshotRetentionPeriod"]).to eq({"Ref"=>"AutomatedSnapshotRetentionPeriod"})
258+
end
259+
260+
it "to have property PreferredMaintenanceWindow" do
261+
expect(resource["Properties"]["PreferredMaintenanceWindow"]).to eq({"Ref"=>"MaintenanceWindow"})
262+
end
263+
264+
it "to have property LoggingProperties" do
265+
expect(resource["Properties"]["LoggingProperties"]).to eq({"Fn::If"=>["EnableLoggingCondition", {"BucketName"=>{"Ref"=>"RedshiftLoggingS3Bucket"}, "S3KeyPrefix"=>"AWSLogs"}, {"Ref"=>"AWS::NoValue"}]})
266+
end
267+
268+
it "to have property IamRoles" do
269+
expect(resource["Properties"]["IamRoles"]).to eq([{"Fn::GetAtt"=>["RedshiftIAMRole", "Arn"]}])
270+
end
271+
272+
it "to have property SnapshotIdentifier" do
273+
expect(resource["Properties"]["SnapshotIdentifier"]).to eq({"Fn::If"=>["SnapshotSet", {"Ref"=>"Snapshot"}, {"Ref"=>"AWS::NoValue"}]})
274+
end
275+
276+
it "to have property OwnerAccount" do
277+
expect(resource["Properties"]["OwnerAccount"]).to eq({"Fn::If"=>["SnapshotAccountOwnerSet", {"Ref"=>"SnapshotAccountOwner"}, {"Ref"=>"AWS::NoValue"}]})
278+
end
279+
280+
it "to have property Tags" do
281+
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}])
282+
end
283+
284+
end
285+
286+
end
287+
288+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
test_metadata:
2+
type: config
3+
name: redshift_federation_iam_role
4+
description: set the description for your test
5+
6+
# Insert your tests here
7+
iam_policies:
8+
redshift-federation:
9+
redshift:
10+
action:
11+
- redshift:CreateClusterUser
12+
- redshift:JoinGroup
13+
- redshift:GetClusterCredentials
14+
- redshift:ListSchemas
15+
- redshift:ListTables
16+
- redshift:ListDatabases
17+
- redshift:ExecuteQuery
18+
- redshift:FetchResults
19+
- redshift:CancelQuery
20+
- redshift:DescribeClusters
21+
- redshift:DescribeQuery
22+
- redshift:DescribeTable
23+
resource:
24+
- Fn::Sub: 'arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:cluster:${RedshiftCluster}'
25+
- Fn::Sub: 'arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbuser:${RedshiftCluster}/${!redshift:DbUser}'
26+
- Fn::Sub: 'arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbname:${RedshiftCluster}/${!redshift:DbName}'
27+
- Fn::Sub: 'arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:dbgroup:${RedshiftCluster}/mygroup'
28+
redshift:
29+
logs:
30+
action:
31+
- logs:*
32+
33+
redshift_federation_iam_role:
34+
enable: true
35+
assume_role_policy:
36+
principal:
37+
providerName: redshift-federation-saml-provider
38+
condition:
39+
samlAud: "http://localhost:7890/redshift/"

0 commit comments

Comments
 (0)