Skip to content

Commit 1ca111a

Browse files
Merge pull request #160 from michaelryanmcneill/ROSA-786
ROSAENG-57757 | fix: apply trust_policy_external_id to support role trust policy
2 parents de80e87 + 3b2c533 commit 1ca111a

5 files changed

Lines changed: 247 additions & 28 deletions

File tree

modules/account-iam-resources/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ No modules.
5656
| [random_string.default_random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource |
5757
| [time_sleep.account_iam_resources_wait](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource |
5858
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
59-
| [aws_iam_policy_document.custom_trust_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
6059
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
6160
| [rhcs_hcp_policies.all_policies](https://registry.terraform.io/providers/terraform-redhat/rhcs/latest/docs/data-sources/hcp_policies) | data source |
6261
| [rhcs_info.current](https://registry.terraform.io/providers/terraform-redhat/rhcs/latest/docs/data-sources/info) | data source |

modules/account-iam-resources/main.tf

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33

44
locals {
55
path = coalesce(var.path, "/")
6+
trust_policy_external_id = (
7+
var.trust_policy_external_id != null && var.trust_policy_external_id != ""
8+
) ? var.trust_policy_external_id : null
69
account_roles_properties = [
710
{
811
role_name = "HCP-ROSA-Installer"
912
role_type = "installer"
1013
policy_details = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/ROSAInstallerPolicy"
1114
principal_type = "AWS"
1215
principal_identifier = "arn:${data.aws_partition.current.partition}:iam::${data.rhcs_info.current.ocm_aws_account_id}:role/RH-Managed-OpenShift-Installer"
13-
external_id = var.trust_policy_external_id
16+
external_id = local.trust_policy_external_id
1417
},
1518
{
1619
role_name = "HCP-ROSA-Support"
@@ -19,7 +22,7 @@ locals {
1922
principal_type = "AWS"
2023
// This is a SRE RH Support role which is used to assume this support role
2124
principal_identifier = data.rhcs_hcp_policies.all_policies.account_role_policies["sts_support_rh_sre_role"]
22-
external_id = null
25+
external_id = local.trust_policy_external_id
2326
},
2427
{
2528
role_name = "HCP-ROSA-Worker"
@@ -45,6 +48,32 @@ locals {
4548
vpce_role_name = local.vpce_splits[length(local.vpce_splits) - 1]
4649
vpce_policy_name = substr("${local.vpce_role_name}-assume-role", 0, 64)
4750
policy_arn_base = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy"
51+
# jsonencode instead of aws_iam_policy_document: trades structural validation for plan-time testability via terraform test.
52+
custom_trust_policy_json = [
53+
for idx in range(local.account_roles_count) : jsonencode({
54+
Version = "2012-10-17"
55+
Statement = [
56+
merge(
57+
{
58+
Effect = "Allow"
59+
Action = "sts:AssumeRole"
60+
Principal = local.account_roles_properties[idx].principal_type == "AWS" ? {
61+
AWS = local.account_roles_properties[idx].principal_identifier
62+
} : {
63+
Service = local.account_roles_properties[idx].principal_identifier
64+
}
65+
},
66+
local.account_roles_properties[idx].external_id != null ? {
67+
Condition = {
68+
StringEquals = {
69+
"sts:ExternalId" = local.account_roles_properties[idx].external_id
70+
}
71+
}
72+
} : {}
73+
)
74+
]
75+
})
76+
]
4877
}
4978

5079
data "aws_caller_identity" "current" {}
@@ -55,35 +84,12 @@ data "aws_partition" "current" {}
5584

5685
data "rhcs_info" "current" {}
5786

58-
data "aws_iam_policy_document" "custom_trust_policy" {
59-
count = local.account_roles_count
60-
61-
statement {
62-
effect = "Allow"
63-
actions = ["sts:AssumeRole"]
64-
principals {
65-
type = local.account_roles_properties[count.index].principal_type
66-
identifiers = [local.account_roles_properties[count.index].principal_identifier]
67-
}
68-
69-
dynamic "condition" {
70-
# Only set this condition if "trust_policy_external_id" is set
71-
for_each = (local.account_roles_properties[count.index].external_id != null) ? [1] : []
72-
content {
73-
test = "StringEquals"
74-
variable = "sts:ExternalId"
75-
values = [local.account_roles_properties[count.index].external_id]
76-
}
77-
}
78-
}
79-
}
80-
8187
resource "aws_iam_role" "account_role" {
8288
count = local.account_roles_count
8389
name = substr("${local.account_role_prefix_valid}-${local.account_roles_properties[count.index].role_name}-Role", 0, 64)
8490
permissions_boundary = var.permissions_boundary
8591
path = local.path
86-
assume_role_policy = data.aws_iam_policy_document.custom_trust_policy[count.index].json
92+
assume_role_policy = local.custom_trust_policy_json[count.index]
8793

8894
tags = merge(var.tags, {
8995
red-hat-managed = true
@@ -165,6 +171,6 @@ resource "time_sleep" "account_iam_resources_wait" {
165171
account_role_prefix = local.account_role_prefix_valid
166172
path = local.path
167173
shared_vpc_policy_attachments = local.route53_shared_role_arn != "" && local.vpce_shared_role_arn != "" ? jsonencode([aws_iam_role_policy_attachment.route53_policy_installer_account_role_attachment[0].policy_arn, aws_iam_role_policy_attachment.vpce_policy_installer_account_role_attachment[0].policy_arn]) : jsonencode([])
168-
trust_policy_external_id = var.trust_policy_external_id
174+
trust_policy_external_id = local.trust_policy_external_id
169175
}
170176
}

modules/account-iam-resources/outputs.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ output "trust_policy_external_id" {
2525
value = time_sleep.account_iam_resources_wait.triggers["trust_policy_external_id"]
2626
description = "External ID for trust policy condition in account roles"
2727
}
28+
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright Red Hat
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
mock_provider "aws" {
5+
alias = "default"
6+
7+
mock_data "aws_partition" {
8+
defaults = {
9+
partition = "aws"
10+
}
11+
}
12+
13+
mock_data "aws_caller_identity" {
14+
defaults = {
15+
account_id = "123456789012"
16+
}
17+
}
18+
19+
mock_resource "aws_iam_role" {
20+
defaults = {
21+
arn = "arn:aws:iam::123456789012:role/mock"
22+
id = "mock-role-id"
23+
name = "mock-role"
24+
}
25+
}
26+
27+
mock_resource "aws_iam_role_policy_attachment" {
28+
defaults = {
29+
id = "mock-attachment-id"
30+
}
31+
}
32+
}
33+
34+
mock_provider "rhcs" {
35+
alias = "default"
36+
37+
mock_data "rhcs_hcp_policies" {
38+
defaults = {
39+
account_role_policies = {
40+
sts_support_rh_sre_role = "arn:aws:iam::999999999999:role/RH-SRE-Support"
41+
}
42+
}
43+
}
44+
45+
mock_data "rhcs_info" {
46+
defaults = {
47+
ocm_aws_account_id = "999999999999"
48+
}
49+
}
50+
}
51+
52+
mock_provider "time" {
53+
alias = "default"
54+
55+
mock_resource "time_sleep" {
56+
defaults = {
57+
id = "mock-sleep"
58+
}
59+
}
60+
}
61+
62+
mock_provider "random" {
63+
alias = "default"
64+
}
65+
66+
variables {
67+
account_role_prefix = "tf-test-acc"
68+
}
69+
70+
run "null_external_id_omits_condition" {
71+
command = plan
72+
73+
providers = {
74+
aws = aws.default
75+
rhcs = rhcs.default
76+
time = time.default
77+
random = random.default
78+
}
79+
80+
variables {
81+
trust_policy_external_id = null
82+
}
83+
84+
assert {
85+
condition = !strcontains(aws_iam_role.account_role[0].assume_role_policy, "sts:ExternalId")
86+
error_message = "Installer trust policy must not include sts:ExternalId when trust_policy_external_id is null."
87+
}
88+
89+
assert {
90+
condition = !strcontains(aws_iam_role.account_role[1].assume_role_policy, "sts:ExternalId")
91+
error_message = "Support trust policy must not include sts:ExternalId when trust_policy_external_id is null."
92+
}
93+
}
94+
95+
run "empty_external_id_rejected" {
96+
command = plan
97+
98+
providers = {
99+
aws = aws.default
100+
rhcs = rhcs.default
101+
time = time.default
102+
random = random.default
103+
}
104+
105+
variables {
106+
trust_policy_external_id = ""
107+
}
108+
109+
expect_failures = [
110+
var.trust_policy_external_id,
111+
]
112+
}
113+
114+
run "whitespace_external_id_rejected" {
115+
command = plan
116+
117+
providers = {
118+
aws = aws.default
119+
rhcs = rhcs.default
120+
time = time.default
121+
random = random.default
122+
}
123+
124+
variables {
125+
trust_policy_external_id = " "
126+
}
127+
128+
expect_failures = [
129+
var.trust_policy_external_id,
130+
]
131+
}
132+
133+
run "non_empty_external_id_adds_condition_to_installer_and_support" {
134+
command = plan
135+
136+
providers = {
137+
aws = aws.default
138+
rhcs = rhcs.default
139+
time = time.default
140+
random = random.default
141+
}
142+
143+
variables {
144+
trust_policy_external_id = "test-external-id-12345"
145+
}
146+
147+
assert {
148+
condition = strcontains(
149+
aws_iam_role.account_role[0].assume_role_policy,
150+
"test-external-id-12345",
151+
)
152+
error_message = "Installer trust policy must include the configured external ID."
153+
}
154+
155+
assert {
156+
condition = strcontains(
157+
aws_iam_role.account_role[1].assume_role_policy,
158+
"test-external-id-12345",
159+
)
160+
error_message = "Support trust policy must include the configured external ID."
161+
}
162+
163+
assert {
164+
condition = strcontains(
165+
aws_iam_role.account_role[0].assume_role_policy,
166+
"sts:ExternalId",
167+
)
168+
error_message = "Installer trust policy must include sts:ExternalId condition key."
169+
}
170+
171+
assert {
172+
condition = strcontains(
173+
aws_iam_role.account_role[1].assume_role_policy,
174+
"sts:ExternalId",
175+
)
176+
error_message = "Support trust policy must include sts:ExternalId condition key."
177+
}
178+
}
179+
180+
run "worker_never_includes_external_id" {
181+
command = plan
182+
183+
providers = {
184+
aws = aws.default
185+
rhcs = rhcs.default
186+
time = time.default
187+
random = random.default
188+
}
189+
190+
variables {
191+
trust_policy_external_id = "test-external-id-12345"
192+
}
193+
194+
assert {
195+
condition = !strcontains(aws_iam_role.account_role[2].assume_role_policy, "sts:ExternalId")
196+
error_message = "Worker trust policy must never include sts:ExternalId."
197+
}
198+
199+
assert {
200+
condition = strcontains(aws_iam_role.account_role[2].assume_role_policy, "ec2.amazonaws.com")
201+
error_message = "Worker role trust policy must trust ec2.amazonaws.com."
202+
}
203+
204+
assert {
205+
condition = length(aws_iam_role.account_role) == 3
206+
error_message = "account_iam_resources must create Installer, Support, and Worker roles."
207+
}
208+
}

modules/account-iam-resources/variables.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ variable "trust_policy_external_id" {
4444
type = string
4545
default = null
4646
description = "External ID for trust policy condition in account roles"
47+
48+
validation {
49+
condition = var.trust_policy_external_id == null ? true : length(trimspace(var.trust_policy_external_id)) > 0
50+
error_message = "trust_policy_external_id must be null or a non-empty, non-whitespace string"
51+
}
4752
}

0 commit comments

Comments
 (0)