From a9ac0f40cd47cfa924e60113d5fe18058145907a Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 4 Apr 2024 10:26:35 +0200 Subject: [PATCH] Add variable to resman to control top-level folder IAM (#2196) --- fast/stages/1-resman/README.md | 25 +++++++------- fast/stages/1-resman/branch-data-platform.tf | 1 + fast/stages/1-resman/branch-gcve.tf | 3 +- fast/stages/1-resman/branch-gke.tf | 3 +- fast/stages/1-resman/branch-networking.tf | 35 ++++++++++++++------ fast/stages/1-resman/branch-sandbox.tf | 28 ++++++++++++---- fast/stages/1-resman/branch-security.tf | 33 ++++++++++++------ fast/stages/1-resman/branch-teams.tf | 26 +++++++++++---- fast/stages/1-resman/branch-tenants.tf | 1 + fast/stages/1-resman/variables.tf | 16 +++++++++ tests/fast/stages/s1_resman/simple.tfvars | 34 +++++++++++++++++++ tests/fast/stages/s1_resman/simple.yaml | 4 +-- 12 files changed, 158 insertions(+), 51 deletions(-) diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index b88d7f0b..13a8c7d1 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -311,7 +311,7 @@ This allows to centralize the minimum set of resources to delegate control of ea ### IAM -IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../0-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies in modules' `iam_groups`, `iam`, and `iam_additive` variables. +The `folder_iam` variable can be used to manage authoritative bindings for all top-level folders. For additional control, IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../0-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies throuth the IAM-related variables (`iam`, `iam_bindings`, `iam_bindings_additive`) of the relevant modules. A full reference of IAM roles managed by this stage [is available here](./IAM.md). @@ -358,21 +358,22 @@ Due to its simplicity, this stage lends itself easily to customizations: adding |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables.tf#L42) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | 0-bootstrap | -| [organization](variables.tf#L228) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables.tf#L244) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [organization](variables.tf#L244) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L260) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | | [cicd_repositories](variables.tf#L53) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | | [custom_roles](variables.tf#L147) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | | [factories_config](variables.tf#L159) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_features](variables.tf#L168) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | -| [groups](variables.tf#L183) | 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.tf#L198) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | -| [org_policy_tags](variables.tf#L216) | Resource management tags for organization policy exceptions. | object({…}) | | {} | 0-bootstrap | -| [outputs_location](variables.tf#L238) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [tag_names](variables.tf#L255) | Customized names for resource management tags. | object({…}) | | {} | | -| [tags](variables.tf#L270) | Custome secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | -| [team_folders](variables.tf#L291) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | -| [tenants](variables.tf#L307) | Lightweight tenant definitions. | map(object({…})) | | {} | | -| [tenants_config](variables.tf#L323) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…}) | | {} | | +| [folder_iam](variables.tf#L183) | Authoritative IAM for top-level folders. | object({…}) | | {} | | +| [groups](variables.tf#L199) | 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.tf#L214) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [org_policy_tags](variables.tf#L232) | Resource management tags for organization policy exceptions. | object({…}) | | {} | 0-bootstrap | +| [outputs_location](variables.tf#L254) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [tag_names](variables.tf#L271) | Customized names for resource management tags. | object({…}) | | {} | | +| [tags](variables.tf#L286) | Custome secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | +| [team_folders](variables.tf#L307) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [tenants](variables.tf#L323) | Lightweight tenant definitions. | map(object({…})) | | {} | | +| [tenants_config](variables.tf#L339) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…}) | | {} | | ## Outputs diff --git a/fast/stages/1-resman/branch-data-platform.tf b/fast/stages/1-resman/branch-data-platform.tf index f4bc2593..0992d2d5 100644 --- a/fast/stages/1-resman/branch-data-platform.tf +++ b/fast/stages/1-resman/branch-data-platform.tf @@ -21,6 +21,7 @@ module "branch-dp-folder" { count = var.fast_features.data_platform ? 1 : 0 parent = "organizations/${var.organization.id}" name = "Data Platform" + iam = var.folder_iam.data_platform tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/data"].id, null diff --git a/fast/stages/1-resman/branch-gcve.tf b/fast/stages/1-resman/branch-gcve.tf index 43b73762..81b39443 100644 --- a/fast/stages/1-resman/branch-gcve.tf +++ b/fast/stages/1-resman/branch-gcve.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ module "branch-gcve-folder" { count = var.fast_features.gcve ? 1 : 0 parent = "organizations/${var.organization.id}" name = "GCVE" + iam = var.folder_iam.gcve tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/gcve"].id, null diff --git a/fast/stages/1-resman/branch-gke.tf b/fast/stages/1-resman/branch-gke.tf index 177e012f..addfa8f6 100644 --- a/fast/stages/1-resman/branch-gke.tf +++ b/fast/stages/1-resman/branch-gke.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ module "branch-gke-folder" { count = var.fast_features.gke ? 1 : 0 parent = "organizations/${var.organization.id}" name = "GKE" + iam = var.folder_iam.gke tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/gke"].id, null diff --git a/fast/stages/1-resman/branch-networking.tf b/fast/stages/1-resman/branch-networking.tf index 5bb641c6..a0e20e6f 100644 --- a/fast/stages/1-resman/branch-networking.tf +++ b/fast/stages/1-resman/branch-networking.tf @@ -16,6 +16,29 @@ # tfdoc:file:description Networking stage resources. +locals { + # FAST-specific IAM + _network_folder_fast_iam = { + # read-write (apply) automation service account + "roles/logging.admin" = [module.branch-network-sa.iam_email] + "roles/owner" = [module.branch-network-sa.iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-network-sa.iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-network-sa.iam_email] + "roles/compute.xpnAdmin" = [module.branch-network-sa.iam_email] + # read-only (plan) automation service account + "roles/viewer" = [module.branch-network-r-sa.iam_email] + "roles/resourcemanager.folderViewer" = [module.branch-network-r-sa.iam_email] + } + # deep-merge FAST-specific IAM with user-provided bindings in var.folder_iam + _network_folder_iam = merge( + var.folder_iam.network, + { + for role, principals in local._network_folder_fast_iam : + role => distinct(concat(principals, lookup(var.folder_iam.network, role, []))) + } + ) +} + module "branch-network-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" @@ -27,17 +50,7 @@ module "branch-network-folder" { "roles/editor", ] } - iam = { - # read-write (apply) automation service account - "roles/logging.admin" = [module.branch-network-sa.iam_email] - "roles/owner" = [module.branch-network-sa.iam_email] - "roles/resourcemanager.folderAdmin" = [module.branch-network-sa.iam_email] - "roles/resourcemanager.projectCreator" = [module.branch-network-sa.iam_email] - "roles/compute.xpnAdmin" = [module.branch-network-sa.iam_email] - # read-only (plan) automation service account - "roles/viewer" = [module.branch-network-r-sa.iam_email] - "roles/resourcemanager.folderViewer" = [module.branch-network-r-sa.iam_email] - } + iam = local._network_folder_iam tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/networking"].id, null diff --git a/fast/stages/1-resman/branch-sandbox.tf b/fast/stages/1-resman/branch-sandbox.tf index 3628df76..98dc59bf 100644 --- a/fast/stages/1-resman/branch-sandbox.tf +++ b/fast/stages/1-resman/branch-sandbox.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,31 @@ # tfdoc:file:description Sandbox stage resources. -module "branch-sandbox-folder" { - source = "../../../modules/folder" - count = var.fast_features.sandbox ? 1 : 0 - parent = "organizations/${var.organization.id}" - name = "Sandbox" - iam = { +locals { + # FAST-specific IAM + _sandbox_folder_fast_iam = !var.fast_features.sandbox ? {} : { "roles/logging.admin" = [module.branch-sandbox-sa.0.iam_email] "roles/owner" = [module.branch-sandbox-sa.0.iam_email] "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.0.iam_email] "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.0.iam_email] } + # deep-merge FAST-specific IAM with user-provided bindings in var.folder_iam + _sandbox_folder_iam = merge( + var.folder_iam.sandbox, + { + for role, principals in local._sandbox_folder_fast_iam : + role => distinct(concat(principals, lookup(var.folder_iam.sandbox, role, []))) + } + ) +} + + +module "branch-sandbox-folder" { + source = "../../../modules/folder" + count = var.fast_features.sandbox ? 1 : 0 + parent = "organizations/${var.organization.id}" + name = "Sandbox" + iam = local._sandbox_folder_iam org_policies = { "sql.restrictPublicIp" = { rules = [{ enforce = false }] } "compute.vmExternalIpAccess" = { rules = [{ allow = { all = true } }] } diff --git a/fast/stages/1-resman/branch-security.tf b/fast/stages/1-resman/branch-security.tf index 5e828812..9051dced 100644 --- a/fast/stages/1-resman/branch-security.tf +++ b/fast/stages/1-resman/branch-security.tf @@ -16,6 +16,28 @@ # tfdoc:file:description Security stage resources. +locals { + # FAST-specific IAM + _security_folder_fast_iam = { + "roles/logging.admin" = [module.branch-security-sa.iam_email] + "roles/owner" = [module.branch-security-sa.iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-security-sa.iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-security-sa.iam_email] + # read-only (plan) automation service account + "roles/viewer" = [module.branch-security-r-sa.iam_email] + "roles/resourcemanager.folderViewer" = [module.branch-security-r-sa.iam_email] + } + + # deep-merge FAST-specific IAM with user-provided bindings in var.folder_iam + _security_folder_iam = merge( + var.folder_iam.security, + { + for role, principals in local._security_folder_fast_iam : + role => distinct(concat(principals, lookup(var.folder_iam.security, role, []))) + } + ) +} + module "branch-security-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" @@ -27,16 +49,7 @@ module "branch-security-folder" { "roles/editor" ] } - iam = { - # read-write (apply) automation service account - "roles/logging.admin" = [module.branch-security-sa.iam_email] - "roles/owner" = [module.branch-security-sa.iam_email] - "roles/resourcemanager.folderAdmin" = [module.branch-security-sa.iam_email] - "roles/resourcemanager.projectCreator" = [module.branch-security-sa.iam_email] - # read-only (plan) automation service account - "roles/viewer" = [module.branch-security-r-sa.iam_email] - "roles/resourcemanager.folderViewer" = [module.branch-security-r-sa.iam_email] - } + iam = local._security_folder_iam tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/security"].id, null diff --git a/fast/stages/1-resman/branch-teams.tf b/fast/stages/1-resman/branch-teams.tf index eba9cb49..70932900 100644 --- a/fast/stages/1-resman/branch-teams.tf +++ b/fast/stages/1-resman/branch-teams.tf @@ -17,19 +17,31 @@ # tfdoc:file:description Team stage resources. # TODO(ludo): add support for CI/CD - -module "branch-teams-folder" { - source = "../../../modules/folder" - count = var.fast_features.teams ? 1 : 0 - parent = "organizations/${var.organization.id}" - name = "Teams" - iam = { +locals { + # FAST-specific IAM + _teams_folder_fast_iam = !var.fast_features.teams ? {} : { "roles/logging.admin" = [module.branch-teams-sa.0.iam_email] "roles/owner" = [module.branch-teams-sa.0.iam_email] "roles/resourcemanager.folderAdmin" = [module.branch-teams-sa.0.iam_email] "roles/resourcemanager.projectCreator" = [module.branch-teams-sa.0.iam_email] "roles/compute.xpnAdmin" = [module.branch-teams-sa.0.iam_email] } + # deep-merge FAST-specific IAM with user-provided bindings in var.folder_iam + _teams_folder_iam = merge( + var.folder_iam.teams, + { + for role, principals in local._teams_folder_fast_iam : + role => distinct(concat(principals, lookup(var.folder_iam.teams, role, []))) + } + ) +} + +module "branch-teams-folder" { + source = "../../../modules/folder" + count = var.fast_features.teams ? 1 : 0 + parent = "organizations/${var.organization.id}" + name = "Teams" + iam = local._teams_folder_iam tag_bindings = { context = try( module.organization.tag_values["${var.tag_names.context}/teams"].id, null diff --git a/fast/stages/1-resman/branch-tenants.tf b/fast/stages/1-resman/branch-tenants.tf index 150f8eee..4d7ffa5c 100644 --- a/fast/stages/1-resman/branch-tenants.tf +++ b/fast/stages/1-resman/branch-tenants.tf @@ -33,6 +33,7 @@ module "tenant-tenants-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" name = "Tenants" + iam = var.folder_iam.tenants tag_bindings = { context = module.organization.tag_values["${var.tag_names.context}/tenant"].id } diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf index 03c6624b..8fb4c8dd 100644 --- a/fast/stages/1-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -180,6 +180,22 @@ variable "fast_features" { nullable = false } +variable "folder_iam" { + description = "Authoritative IAM for top-level folders." + type = object({ + data_platform = optional(map(list(string)), {}) + gcve = optional(map(list(string)), {}) + gke = optional(map(list(string)), {}) + sandbox = optional(map(list(string)), {}) + security = optional(map(list(string)), {}) + network = optional(map(list(string)), {}) + teams = optional(map(list(string)), {}) + tenants = optional(map(list(string)), {}) + }) + nullable = false + default = {} +} + variable "groups" { # tfdoc:variable:source 0-bootstrap # https://cloud.google.com/docs/enterprise/setup-checklist diff --git a/tests/fast/stages/s1_resman/simple.tfvars b/tests/fast/stages/s1_resman/simple.tfvars index 728ad10e..9cb85288 100644 --- a/tests/fast/stages/s1_resman/simple.tfvars +++ b/tests/fast/stages/s1_resman/simple.tfvars @@ -32,3 +32,37 @@ organization = { customer_id = "C00000000" } prefix = "fast2" +folder_iam = { + data_platform = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + gcve = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + gke = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + sandbox = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + security = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + network = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + teams = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } + tenants = { + "roles/owner" = ["user:extra-owner@fast.example.com"] + "roles/browser" = ["user:extra-browser@fast.example.com"] + } +} diff --git a/tests/fast/stages/s1_resman/simple.yaml b/tests/fast/stages/s1_resman/simple.yaml index b0fea5a7..0f2aa097 100644 --- a/tests/fast/stages/s1_resman/simple.yaml +++ b/tests/fast/stages/s1_resman/simple.yaml @@ -14,7 +14,7 @@ counts: google_folder: 5 - google_folder_iam_binding: 21 + google_folder_iam_binding: 25 google_organization_iam_member: 5 google_project_iam_member: 4 google_service_account: 4 @@ -27,4 +27,4 @@ counts: google_tags_tag_key: 3 google_tags_tag_value: 10 modules: 12 - resources: 76 + resources: 80