Skip to content

Commit d94f4c7

Browse files
ROSAENG-57757 | fix: apply trust_policy_external_id to support role trust policy
Align support account role with installer when trust_policy_external_id is set and normalize empty strings to unset for external ID injection. Part of ROSA-786. Signed-off-by: michaelryanmcneill <michael@michaelryanmcneill.com>
1 parent 8f49326 commit d94f4c7

5 files changed

Lines changed: 251 additions & 28 deletions

File tree

modules/account-iam-resources/README.md

Lines changed: 1 addition & 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 |
@@ -79,6 +78,7 @@ No modules.
7978
| ---- | ----------- |
8079
| <a name="output_account_role_prefix"></a> [account\_role\_prefix](#output\_account\_role\_prefix) | The prefix used for all generated AWS resources. |
8180
| <a name="output_account_roles_arn"></a> [account\_roles\_arn](#output\_account\_roles\_arn) | A map of Amazon Resource Names (ARNs) associated with the AWS IAM roles created. The key in the map represents the name of an AWS IAM role, while the corresponding value represents the associated Amazon Resource Name (ARN) of that role. |
81+
| <a name="output_custom_trust_policy_json"></a> [custom\_trust\_policy\_json](#output\_custom\_trust\_policy\_json) | Rendered IAM trust policy JSON for each account role (HCP-ROSA-Installer, HCP-ROSA-Support, HCP-ROSA-Worker order). |
8282
| <a name="output_path"></a> [path](#output\_path) | The arn path for the account/operator roles as well as their policies. |
8383
| <a name="output_shared_vpc_policy_attachments"></a> [shared\_vpc\_policy\_attachments](#output\_shared\_vpc\_policy\_attachments) | A list of Amazon Resource Names (ARNs) related to the shared VPC |
8484
| <a name="output_trust_policy_external_id"></a> [trust\_policy\_external\_id](#output\_trust\_policy\_external\_id) | External ID for trust policy condition in account roles |

modules/account-iam-resources/main.tf

Lines changed: 32 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,31 @@ 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+
custom_trust_policy_json = [
52+
for idx in range(local.account_roles_count) : jsonencode({
53+
Version = "2012-10-17"
54+
Statement = [
55+
merge(
56+
{
57+
Effect = "Allow"
58+
Action = "sts:AssumeRole"
59+
Principal = local.account_roles_properties[idx].principal_type == "AWS" ? {
60+
AWS = local.account_roles_properties[idx].principal_identifier
61+
} : {
62+
Service = local.account_roles_properties[idx].principal_identifier
63+
}
64+
},
65+
local.account_roles_properties[idx].external_id != null ? {
66+
Condition = {
67+
StringEquals = {
68+
"sts:ExternalId" = local.account_roles_properties[idx].external_id
69+
}
70+
}
71+
} : {}
72+
)
73+
]
74+
})
75+
]
4876
}
4977

5078
data "aws_caller_identity" "current" {}
@@ -55,35 +83,12 @@ data "aws_partition" "current" {}
5583

5684
data "rhcs_info" "current" {}
5785

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-
8186
resource "aws_iam_role" "account_role" {
8287
count = local.account_roles_count
8388
name = substr("${local.account_role_prefix_valid}-${local.account_roles_properties[count.index].role_name}-Role", 0, 64)
8489
permissions_boundary = var.permissions_boundary
8590
path = local.path
86-
assume_role_policy = data.aws_iam_policy_document.custom_trust_policy[count.index].json
91+
assume_role_policy = local.custom_trust_policy_json[count.index]
8792

8893
tags = merge(var.tags, {
8994
red-hat-managed = true
@@ -165,6 +170,6 @@ resource "time_sleep" "account_iam_resources_wait" {
165170
account_role_prefix = local.account_role_prefix_valid
166171
path = local.path
167172
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
173+
trust_policy_external_id = local.trust_policy_external_id
169174
}
170175
}

modules/account-iam-resources/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ 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+
29+
output "custom_trust_policy_json" {
30+
value = local.custom_trust_policy_json
31+
description = "Rendered IAM trust policy JSON for each account role (HCP-ROSA-Installer, HCP-ROSA-Support, HCP-ROSA-Worker order)."
32+
}
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(output.custom_trust_policy_json[0], "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(output.custom_trust_policy_json[1], "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+
output.custom_trust_policy_json[0],
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+
output.custom_trust_policy_json[1],
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+
output.custom_trust_policy_json[0],
166+
"sts:ExternalId",
167+
)
168+
error_message = "Installer trust policy must include sts:ExternalId condition key."
169+
}
170+
171+
assert {
172+
condition = strcontains(
173+
output.custom_trust_policy_json[1],
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(output.custom_trust_policy_json[2], "sts:ExternalId")
196+
error_message = "Worker trust policy must never include sts:ExternalId."
197+
}
198+
199+
assert {
200+
condition = strcontains(output.custom_trust_policy_json[2], "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)