From e4941c27f2afa053a8dd8a839b026963858a1642 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 13 May 2024 20:18:51 +0200 Subject: [PATCH] Implement the full IAM interface for tags (#2269) * IAM authoritative bindings in org module * remove extra newline * organization module * project module * tfdoc --- modules/kms/README.md | 6 +- modules/kms/variables.tf | 1 - modules/organization/README.md | 47 ++++- modules/organization/tags.tf | 166 ++++++++++++++---- modules/organization/variables-tags.tf | 80 ++++++++- modules/project/README.md | 8 +- modules/project/tags.tf | 166 ++++++++++++++---- modules/project/variables-tags.tf | 83 ++++++++- tests/modules/organization/examples/tags.yaml | 27 ++- 9 files changed, 492 insertions(+), 92 deletions(-) diff --git a/modules/kms/README.md b/modules/kms/README.md index 4782d8240d..90621bef37 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -120,14 +120,14 @@ module "kms" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [keyring](variables.tf#L64) | Keyring attributes. | object({…}) | ✓ | | -| [project_id](variables.tf#L115) | Project id where the keyring will be created. | string | ✓ | | +| [project_id](variables.tf#L114) | Project id where the keyring will be created. | string | ✓ | | | [iam](variables.tf#L17) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [import_job](variables.tf#L54) | Keyring import job attributes. | object({…}) | | null | | [keyring_create](variables.tf#L72) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | -| [keys](variables.tf#L78) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L120) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | +| [keys](variables.tf#L78) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L119) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf index 2708a7f7cf..d0d335674b 100644 --- a/modules/kms/variables.tf +++ b/modules/kms/variables.tf @@ -87,7 +87,6 @@ variable "keys" { algorithm = string protection_level = optional(string, "SOFTWARE") })) - iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) diff --git a/modules/organization/README.md b/modules/organization/README.md index 1b004aa623..f0253d11b3 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -440,12 +440,44 @@ module "org" { iam = { "roles/resourcemanager.tagAdmin" = ["group:${var.group_email}"] } + iam_bindings = { + viewer = { + role = "roles/resourcemanager.tagViewer" + members = ["group:gcp-support@example.org"] + } + } + iam_bindings_additive = { + user_app1 = { + role = "roles/resourcemanager.tagUser" + member = "group:app1-team@example.org" + } + } values = { - dev = {} + dev = { + iam_bindings_additive = { + user_app2 = { + role = "roles/resourcemanager.tagUser" + member = "group:app2-team@example.org" + } + } + } prod = { description = "Environment: production." iam = { - "roles/resourcemanager.tagViewer" = ["group:${var.group_email}"] + "roles/resourcemanager.tagViewer" = ["group:app1-team@example.org"] + } + iam_bindings = { + admin = { + role = "roles/resourcemanager.tagAdmin" + members = ["group:gcp-support@example.org"] + condition = { + title = "gcp_support" + expression = <<-END + request.time.getHours("Europe/Berlin") <= 9 && + request.time.getHours("Europe/Berlin") >= 17 + END + } + } } } } @@ -455,8 +487,9 @@ module "org" { env-prod = module.org.tag_values["environment/prod"].id } } -# tftest modules=1 resources=6 inventory=tags.yaml e2e serial +# tftest modules=1 resources=10 inventory=tags.yaml ``` + You can also define network tags, through a dedicated variable *network_tags*: @@ -498,7 +531,7 @@ module "org" { | [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint | | [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | -| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-logging.tf](./variables-logging.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | @@ -522,11 +555,11 @@ module "org" { | [logging_exclusions](variables-logging.tf#L32) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L39) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L49) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [org_policies](variables.tf#L51) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | | [org_policy_custom_constraints](variables.tf#L78) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | -| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | -| [tags](variables-tags.tf#L52) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [tag_bindings](variables-tags.tf#L81) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | +| [tags](variables-tags.tf#L88) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | ## Outputs diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index d25757c243..0321818a5a 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -15,50 +15,96 @@ */ locals { - _tag_values = flatten([ - for tag, attrs in local.tags : [ - for value, value_attrs in attrs.values : { - description = value_attrs.description, - key = "${tag}/${value}" - id = try(value_attrs.id, null) - name = value - roles = keys(value_attrs.iam) - tag = tag - tag_id = attrs.id - tag_network = try(attrs.network, null) != null + _tag_iam = flatten([ + for k, v in local.tags : [ + for role in keys(v.iam) : { + # we cycle on keys here so we don't risk injecting dynamic values + role = role + tag = k + tag_id = v.id } ] ]) - _tag_values_iam = flatten([ - for key, value_attrs in local.tag_values : [ - for role in value_attrs.roles : { - id = value_attrs.id - key = value_attrs.key - name = value_attrs.name + _tag_value_iam = flatten([ + for k, v in local.tag_values : [ + for role in v.roles : { + id = v.id + key = v.key + name = v.name role = role - tag = value_attrs.tag + tag = v.tag } ] ]) - _tags_iam = flatten([ - for tag, attrs in local.tags : [ - for role in keys(attrs.iam) : { - role = role - tag = tag - tag_id = attrs.id + _tag_values = flatten([ + for k, v in local.tags : [ + for vk, vv in v.values : { + description = vv.description, + key = "${k}/${vk}" + iam_bindings = keys(vv.iam_bindings) + iam_bindings_additive = keys(vv.iam_bindings_additive) + id = try(vv.id, null) + name = vk + # we only store keys here so we don't risk injecting dynamic values + roles = keys(vv.iam) + tag = k + tag_id = v.id + tag_network = try(v.network, null) != null } ] ]) - tag_values = { - for t in local._tag_values : t.key => t + tag_iam = { + for t in local._tag_iam : "${t.tag}:${t.role}" => t + } + tag_iam_bindings = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_iam_bindings_additive = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings_additive) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_value_iam = { + for v in local._tag_value_iam : "${v.key}:${v.role}" => v } - tag_values_iam = { - for t in local._tag_values_iam : "${t.key}:${t.role}" => t + tag_value_iam_bindings = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_value_iam_bindings_additive = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings_additive : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_values = { + for v in local._tag_values : v.key => v } tags = merge(var.tags, var.network_tags) - tags_iam = { - for t in local._tags_iam : "${t.tag}:${t.role}" => t - } } # keys @@ -82,7 +128,7 @@ resource "google_tags_tag_key" "default" { } resource "google_tags_tag_key_iam_binding" "default" { - for_each = local.tags_iam + for_each = local.tag_iam tag_key = ( each.value.tag_id == null ? google_tags_tag_key.default[each.value.tag].id @@ -94,6 +140,30 @@ resource "google_tags_tag_key_iam_binding" "default" { ) } +resource "google_tags_tag_key_iam_binding" "bindings" { + for_each = local.tag_iam_bindings + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role + members = ( + local.tags[each.value.tag]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_key_iam_member" "bindings" { + for_each = local.tag_iam_bindings_additive + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member +} + # values resource "google_tags_tag_value" "default" { @@ -108,7 +178,7 @@ resource "google_tags_tag_value" "default" { } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam + for_each = local.tag_value_iam tag_value = ( each.value.id == null ? google_tags_tag_value.default[each.value.key].id @@ -121,6 +191,36 @@ resource "google_tags_tag_value_iam_binding" "default" { ) } +resource "google_tags_tag_value_iam_binding" "bindings" { + for_each = local.tag_value_iam_bindings + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role + ) + members = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_value_iam_member" "bindings" { + for_each = local.tag_value_iam_bindings_additive + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role + ) + member = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member + ) +} + # bindings resource "google_tags_tag_binding" "binding" { diff --git a/modules/organization/variables-tags.tf b/modules/organization/variables-tags.tf index 4688504d57..21a0efe6f4 100644 --- a/modules/organization/variables-tags.tf +++ b/modules/organization/variables-tags.tf @@ -19,11 +19,47 @@ variable "network_tags" { type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) - network = string # project_id/vpc_name + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) + network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })), {}) })) nullable = false @@ -54,11 +90,47 @@ variable "tags" { type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/modules/project/README.md b/modules/project/README.md index 75388aefc3..cd92870a47 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -1173,7 +1173,7 @@ module "bucket" { | [quotas.tf](./quotas.tf) | None | google_cloud_quotas_quota_preference | | [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | | [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_compute_subnetwork_iam_member · google_project_iam_member | -| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-quotas.tf](./variables-quotas.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | @@ -1204,7 +1204,7 @@ module "bucket" { | [logging_exclusions](variables.tf#L108) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L115) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L146) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [org_policies](variables.tf#L158) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | | [parent](variables.tf#L185) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | | [prefix](variables.tf#L195) | Optional prefix used to generate project id and name. | string | | null | @@ -1216,8 +1216,8 @@ module "bucket" { | [shared_vpc_host_config](variables.tf#L235) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | | [shared_vpc_service_config](variables.tf#L244) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | | [skip_delete](variables.tf#L272) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | -| [tags](variables-tags.tf#L51) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [tag_bindings](variables-tags.tf#L81) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [tags](variables-tags.tf#L88) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [vpc_sc](variables.tf#L278) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs diff --git a/modules/project/tags.tf b/modules/project/tags.tf index 4f2a666459..1f9ff378b5 100644 --- a/modules/project/tags.tf +++ b/modules/project/tags.tf @@ -15,50 +15,96 @@ */ locals { - _tag_values = flatten([ - for tag, attrs in local.tags : [ - for value, value_attrs in attrs.values : { - description = value_attrs.description, - key = "${tag}/${value}" - id = try(value_attrs.id, null) - name = value - roles = keys(value_attrs.iam) - tag = tag - tag_id = attrs.id - tag_network = try(attrs.network, null) != null + _tag_iam = flatten([ + for k, v in local.tags : [ + for role in keys(v.iam) : { + # we cycle on keys here so we don't risk injecting dynamic values + role = role + tag = k + tag_id = v.id } ] ]) - _tag_values_iam = flatten([ - for key, value_attrs in local.tag_values : [ - for role in value_attrs.roles : { - id = value_attrs.id - key = value_attrs.key - name = value_attrs.name + _tag_value_iam = flatten([ + for k, v in local.tag_values : [ + for role in v.roles : { + id = v.id + key = v.key + name = v.name role = role - tag = value_attrs.tag + tag = v.tag } ] ]) - _tags_iam = flatten([ - for tag, attrs in local.tags : [ - for role in keys(attrs.iam) : { - role = role - tag = tag - tag_id = attrs.id + _tag_values = flatten([ + for k, v in local.tags : [ + for vk, vv in v.values : { + description = vv.description, + key = "${k}/${vk}" + iam_bindings = keys(vv.iam_bindings) + iam_bindings_additive = keys(vv.iam_bindings_additive) + id = try(vv.id, null) + name = vk + # we only store keys here so we don't risk injecting dynamic values + roles = keys(vv.iam) + tag = k + tag_id = v.id + tag_network = try(v.network, null) != null } ] ]) - tag_values = { - for t in local._tag_values : t.key => t + tag_iam = { + for t in local._tag_iam : "${t.tag}:${t.role}" => t + } + tag_iam_bindings = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_iam_bindings_additive = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings_additive) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_value_iam = { + for v in local._tag_value_iam : "${v.key}:${v.role}" => v } - tag_values_iam = { - for t in local._tag_values_iam : "${t.key}:${t.role}" => t + tag_value_iam_bindings = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_value_iam_bindings_additive = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings_additive : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_values = { + for v in local._tag_values : v.key => v } tags = merge(var.tags, var.network_tags) - tags_iam = { - for t in local._tags_iam : "${t.tag}:${t.role}" => t - } } # keys @@ -82,7 +128,7 @@ resource "google_tags_tag_key" "default" { } resource "google_tags_tag_key_iam_binding" "default" { - for_each = local.tags_iam + for_each = local.tag_iam tag_key = ( each.value.tag_id == null ? google_tags_tag_key.default[each.value.tag].id @@ -94,6 +140,30 @@ resource "google_tags_tag_key_iam_binding" "default" { ) } +resource "google_tags_tag_key_iam_binding" "bindings" { + for_each = local.tag_iam_bindings + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role + members = ( + local.tags[each.value.tag]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_key_iam_member" "bindings" { + for_each = local.tag_iam_bindings_additive + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member +} + # values resource "google_tags_tag_value" "default" { @@ -108,7 +178,7 @@ resource "google_tags_tag_value" "default" { } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam + for_each = local.tag_value_iam tag_value = ( each.value.id == null ? google_tags_tag_value.default[each.value.key].id @@ -121,6 +191,36 @@ resource "google_tags_tag_value_iam_binding" "default" { ) } +resource "google_tags_tag_value_iam_binding" "bindings" { + for_each = local.tag_value_iam_bindings + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role + ) + members = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_value_iam_member" "bindings" { + for_each = local.tag_value_iam_bindings_additive + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role + ) + member = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member + ) +} + # bindings resource "google_tags_tag_binding" "binding" { diff --git a/modules/project/variables-tags.tf b/modules/project/variables-tags.tf index 8914aae6e6..ac73f03fd8 100644 --- a/modules/project/variables-tags.tf +++ b/modules/project/variables-tags.tf @@ -19,11 +19,47 @@ variable "network_tags" { type = map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) - network = string # project_id/vpc_name + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) + network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })), {}) })) nullable = false @@ -45,7 +81,8 @@ variable "network_tags" { variable "tag_bindings" { description = "Tag bindings for this project, in key => tag value id format." type = map(string) - default = null + # we need default null here for the project factory module + default = null } variable "tags" { @@ -53,11 +90,47 @@ variable "tags" { type = map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/tests/modules/organization/examples/tags.yaml b/tests/modules/organization/examples/tags.yaml index bed4b46188..cce3674346 100644 --- a/tests/modules/organization/examples/tags.yaml +++ b/tests/modules/organization/examples/tags.yaml @@ -23,11 +23,20 @@ values: purpose_data: null short_name: environment timeouts: null + module.org.google_tags_tag_key_iam_binding.bindings["environment:viewer"]: + condition: [] + members: + - group:gcp-support@example.org + role: roles/resourcemanager.tagViewer module.org.google_tags_tag_key_iam_binding.default["environment:roles/resourcemanager.tagAdmin"]: condition: [] members: - group:organization-admins@example.org role: roles/resourcemanager.tagAdmin + module.org.google_tags_tag_key_iam_member.bindings["environment:user_app1"]: + condition: [] + member: group:app1-team@example.org + role: roles/resourcemanager.tagUser module.org.google_tags_tag_value.default["environment/dev"]: description: Managed by the Terraform organization module. short_name: dev @@ -36,14 +45,28 @@ values: description: 'Environment: production.' short_name: prod timeouts: null + module.org.google_tags_tag_value_iam_binding.bindings["environment/prod:admin"]: + condition: [] + members: + - group:gcp-support@example.org + role: roles/resourcemanager.tagAdmin module.org.google_tags_tag_value_iam_binding.default["environment/prod:roles/resourcemanager.tagViewer"]: condition: [] members: - - group:organization-admins@example.org + - group:app1-team@example.org role: roles/resourcemanager.tagViewer + module.org.google_tags_tag_value_iam_member.bindings["environment/dev:user_app2"]: + condition: [] + member: group:app2-team@example.org + role: roles/resourcemanager.tagUser counts: google_tags_tag_binding: 1 google_tags_tag_key: 1 - google_tags_tag_key_iam_binding: 1 + google_tags_tag_key_iam_binding: 2 + google_tags_tag_key_iam_member: 1 google_tags_tag_value: 2 + google_tags_tag_value_iam_binding: 2 + google_tags_tag_value_iam_member: 1 + modules: 1 + resources: 10