diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md
index 3111cd8b0..0a1499dec 100644
--- a/fast/stages/1-resman/README.md
+++ b/fast/stages/1-resman/README.md
@@ -153,7 +153,7 @@ Tags can also be specified via a factory in a similar way to organization polici
A specific set of tag values used in org-level organization policy conditions can be optionally defined in the bootstrap stage, and can be referenced here if stage service accounts need specific permissions on those.
-As an example, consider this tag value defined via the boostrap stage tfvars.
+As an example, consider this tag value defined via the bootstrap stage tfvars.
```tfvars
org_policies_config = {
@@ -165,7 +165,7 @@ org_policies_config = {
}
```
-The tag is then used in the boostrap stage to modify the behaviour of the relevant organization policy.
+The tag is then used in the bootstrap stage to modify the behaviour of the relevant organization policy.
```yaml
storage.publicAccessPrevention:
@@ -329,8 +329,8 @@ terraform apply
| [custom_roles](variables-fast.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap |
| [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | |
| [fast_addon](variables-addons.tf#L17) | FAST addons configurations for stages 2. Keys are used as short names for the add-on resources. | map(object({…})) | | {} | |
-| [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | map(object({…})) | | {} | |
-| [fast_stage_3](variables-stages.tf#L117) | FAST stages 3 configurations. | map(object({…})) | | {} | |
+| [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | map(object({…})) | | {} | |
+| [fast_stage_3](variables-stages.tf#L125) | FAST stages 3 configurations. | map(object({…})) | | {} | |
| [groups](variables-fast.tf#L93) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap |
| [locations](variables-fast.tf#L109) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap |
| [org_policy_tags](variables-fast.tf#L153) | Organization policy tags. | object({…}) | | {} | 0-bootstrap |
diff --git a/fast/stages/1-resman/stage-3.tf b/fast/stages/1-resman/stage-3.tf
index 3a5af762d..97e4f7c39 100644
--- a/fast/stages/1-resman/stage-3.tf
+++ b/fast/stages/1-resman/stage-3.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -60,6 +60,16 @@ locals {
},
var.fast_stage_3
)
+ _stage_3_iam = { for k, v in local._stage3 : k => {
+ "roles/logging.admin" = [module.stage3-sa-rw[k].iam_email]
+ "roles/owner" = [module.stage3-sa-rw[k].iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.stage3-sa-rw[k].iam_email]
+ "roles/resourcemanager.projectCreator" = [module.stage3-sa-rw[k].iam_email]
+ "roles/compute.xpnAdmin" = [module.stage3-sa-rw[k].iam_email]
+ "roles/viewer" = [module.stage3-sa-ro[k].iam_email]
+ "roles/resourcemanager.folderViewer" = [module.stage3-sa-ro[k].iam_email]
+ }
+ }
# normalize attributes
stage3 = {
for k, v in local._stage3 : k => merge(v, {
@@ -78,7 +88,7 @@ locals {
members = [
for m in vv.members : contains(["ro", "rw"], m) ? "${k}-${m}" : m
]
- condition = vv.condition == null ? null : {
+ condition = lookup(vv, "condition", null) == null ? null : {
title = vv.condition.title
expression = templatestring(vv.condition.expression, {
custom_roles = var.custom_roles
@@ -95,7 +105,7 @@ locals {
kk => {
role = vv.role
member = contains(["ro", "rw"], vv.member) ? "${k}-${vv.member}" : vv.member
- condition = vv.condition == null ? null : {
+ condition = lookup(vv, "condition", null) == null ? null : {
title = vv.condition.title
expression = templatestring(vv.condition.expression, {
custom_roles = var.custom_roles
@@ -107,6 +117,10 @@ locals {
}
}
}
+ iam_by_principals = {
+ for kk, vv in v.folder_config.iam_by_principals :
+ (contains(["ro", "rw"], kk) ? "${k}-${kk}" : kk) => vv
+ }
})
})
if !contains(
@@ -144,17 +158,40 @@ module "stage3-folder" {
)
name = each.value.folder_config.name
iam = {
- "roles/logging.admin" = [module.stage3-sa-rw[each.key].iam_email]
- "roles/owner" = [module.stage3-sa-rw[each.key].iam_email]
- "roles/resourcemanager.folderAdmin" = [module.stage3-sa-rw[each.key].iam_email]
- "roles/resourcemanager.projectCreator" = [module.stage3-sa-rw[each.key].iam_email]
- "roles/compute.xpnAdmin" = [module.stage3-sa-rw[each.key].iam_email]
- "roles/viewer" = [module.stage3-sa-ro[each.key].iam_email]
- "roles/resourcemanager.folderViewer" = [module.stage3-sa-ro[each.key].iam_email]
-
+ # merge inputs/factory bindings with static role bindings in loocal._stage_3_iam
+ for role in concat(keys(each.value.folder_config.iam), keys(local._stage_3_iam[each.key])) :
+ lookup(var.custom_roles, role, role) => [
+ for m in concat(
+ lookup(local._stage_3_iam[each.key], role, []),
+ lookup(each.value.folder_config.iam, role, [])
+ ) : lookup(local.principals_iam, m, m)
+ ]
}
- iam_by_principals = each.value.folder_config.iam_by_principals
- org_policies = each.value.folder_config.org_policies
+
+ iam_bindings = {
+ for k, v in each.value.folder_config.iam_bindings : k => merge(v, {
+ members = [
+ for m in v.members : lookup(local.principals_iam, m, m)
+ ]
+ role = lookup(var.custom_roles, v.role, v.role)
+ condition = v.condition
+ })
+ }
+ iam_bindings_additive = {
+ for k, v in each.value.folder_config.iam_bindings_additive : k => merge(v, {
+ member = lookup(local.principals_iam, v.member, v.member)
+ role = lookup(var.custom_roles, v.role, v.role)
+ condition = v.condition
+ })
+ }
+ iam_by_principals = {
+ for k, v in each.value.folder_config.iam_by_principals :
+ lookup(local.principals_iam, k, k) => [
+ for r in v : lookup(var.custom_roles, r, r)
+ ]
+ }
+
+ org_policies = each.value.folder_config.org_policies
tag_bindings = merge(
{
(var.tag_names.environment) = local.tag_values["${var.tag_names.environment}/${var.environments[each.value.environment].tag_name}"].id
diff --git a/fast/stages/1-resman/variables-stages.tf b/fast/stages/1-resman/variables-stages.tf
index 74e04ae27..ef52cbbac 100644
--- a/fast/stages/1-resman/variables-stages.tf
+++ b/fast/stages/1-resman/variables-stages.tf
@@ -34,7 +34,15 @@ variable "fast_stage_2" {
parent_id = optional(string)
create_env_folders = optional(bool, true)
iam = optional(map(list(string)), {})
- iam_bindings = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
iam_bindings_additive = optional(map(object({
member = string
role = string
@@ -136,7 +144,15 @@ variable "fast_stage_3" {
parent_id = optional(string)
tag_bindings = optional(map(string), {})
iam = optional(map(list(string)), {})
- iam_bindings = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
iam_bindings_additive = optional(map(object({
member = string
role = string