From 80193cfa2e3bd1c98a7bfbca3aaf457321c067e0 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sun, 7 Sep 2025 15:15:27 +0200 Subject: [PATCH] add support for context in kms module (#3307) --- modules/kms/README.md | 19 +++--- modules/kms/iam.tf | 51 +++++++++----- modules/kms/main.tf | 12 +++- modules/kms/tags.tf | 4 +- modules/kms/variables.tf | 24 +++++++ tests/modules/kms/context.tfvars | 111 ++++++++++++++++++++++++++++++ tests/modules/kms/context.yaml | 114 +++++++++++++++++++++++++++++++ tests/modules/kms/tftest.yaml | 17 +++++ 8 files changed, 322 insertions(+), 30 deletions(-) create mode 100644 tests/modules/kms/context.tfvars create mode 100644 tests/modules/kms/context.yaml create mode 100644 tests/modules/kms/tftest.yaml diff --git a/modules/kms/README.md b/modules/kms/README.md index 59979af95..9174ed700 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -154,15 +154,16 @@ module "kms" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [keyring](variables.tf#L64) | Keyring attributes. | object({…}) | ✓ | | -| [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#L119) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | +| [keyring](variables.tf#L88) | Keyring attributes. | object({…}) | ✓ | | +| [project_id](variables.tf#L138) | Project id where the keyring will be created. | string | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [iam](variables.tf#L41) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L48) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L63) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [import_job](variables.tf#L78) | Keyring import job attributes. | object({…}) | | null | +| [keyring_create](variables.tf#L96) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | +| [keys](variables.tf#L102) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L143) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/kms/iam.tf b/modules/kms/iam.tf index 8ac17a567..e867e6ea0 100644 --- a/modules/kms/iam.tf +++ b/modules/kms/iam.tf @@ -51,19 +51,26 @@ locals { resource "google_kms_key_ring_iam_binding" "authoritative" { for_each = var.iam key_ring_id = local.keyring.id - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [ + for v in each.value : + lookup(local.ctx.iam_principals, v, v) + ] } resource "google_kms_key_ring_iam_binding" "bindings" { for_each = var.iam_bindings key_ring_id = local.keyring.id - role = each.value.role - members = each.value.members + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + members = [ + for v in each.value.members : lookup(local.ctx.iam_principals, v, v) + ] dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } @@ -73,12 +80,14 @@ resource "google_kms_key_ring_iam_binding" "bindings" { resource "google_kms_key_ring_iam_member" "bindings" { for_each = var.iam_bindings_additive key_ring_id = local.keyring.id - role = each.value.role - member = each.value.member + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup(local.ctx.iam_principals, each.value.member, each.value.member) dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } @@ -90,20 +99,28 @@ resource "google_kms_crypto_key_iam_binding" "authoritative" { for binding in local.key_iam : "${binding.key}.${binding.role}" => binding } - role = each.value.role + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) crypto_key_id = google_kms_crypto_key.default[each.value.key].id - members = each.value.members + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] } resource "google_kms_crypto_key_iam_binding" "bindings" { for_each = local.key_iam_bindings - role = each.value.role + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) crypto_key_id = google_kms_crypto_key.default[each.value.key].id - members = each.value.members + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } @@ -113,12 +130,14 @@ resource "google_kms_crypto_key_iam_binding" "bindings" { resource "google_kms_crypto_key_iam_member" "members" { for_each = local.key_iam_bindings_additive crypto_key_id = google_kms_crypto_key.default[each.value.key].id - role = each.value.role - member = each.value.member + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup(local.ctx.iam_principals, each.value.member, each.value.member) dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } diff --git a/modules/kms/main.tf b/modules/kms/main.tf index d8b6819a6..61d8f8892 100644 --- a/modules/kms/main.tf +++ b/modules/kms/main.tf @@ -15,6 +15,12 @@ */ locals { + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } + ctx_p = "$" keyring = ( var.keyring_create ? google_kms_key_ring.default[0] @@ -26,14 +32,14 @@ data "google_kms_key_ring" "default" { count = var.keyring_create ? 0 : 1 project = var.project_id name = var.keyring.name - location = var.keyring.location + location = lookup(local.ctx.locations, var.keyring.location, var.keyring.location) } resource "google_kms_key_ring" "default" { count = var.keyring_create ? 1 : 0 project = var.project_id name = var.keyring.name - location = var.keyring.location + location = lookup(local.ctx.locations, var.keyring.location, var.keyring.location) } resource "google_kms_crypto_key" "default" { @@ -61,4 +67,4 @@ resource "google_kms_key_ring_import_job" "default" { import_job_id = var.import_job.id import_method = var.import_job.import_method protection_level = var.import_job.protection_level -} \ No newline at end of file +} diff --git a/modules/kms/tags.tf b/modules/kms/tags.tf index eaee171de..38425156b 100644 --- a/modules/kms/tags.tf +++ b/modules/kms/tags.tf @@ -17,6 +17,6 @@ resource "google_tags_location_tag_binding" "binding" { for_each = var.tag_bindings parent = "//cloudkms.googleapis.com/${local.keyring.id}" - tag_value = each.value - location = var.keyring.location + tag_value = lookup(local.ctx.tag_values, each.value, each.value) + location = lookup(local.ctx.locations, var.keyring.location, var.keyring.location) } diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf index fabaf0322..36fb6d53d 100644 --- a/modules/kms/variables.tf +++ b/modules/kms/variables.tf @@ -14,6 +14,30 @@ * limitations under the License. */ +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + kms_keys = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + tag_keys = optional(map(string), {}) + tag_values = optional(map(string), {}) + }) + default = {} + nullable = false +} + +# variable "factories_config" { +# description = "Paths to data files and folders that enable factory functionality." +# type = object({ +# keyrings = optional(string) +# }) +# nullable = false +# default = {} +# } + variable "iam" { description = "Keyring IAM bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) diff --git a/tests/modules/kms/context.tfvars b/tests/modules/kms/context.tfvars new file mode 100644 index 000000000..4eeca0f38 --- /dev/null +++ b/tests/modules/kms/context.tfvars @@ -0,0 +1,111 @@ +context = { + condition_vars = { + organization = { + id = 1234567890 + } + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + myrole_two = "organizations/366118655033/roles/myRoleTwo" + myrole_three = "organizations/366118655033/roles/myRoleThree" + myrole_four = "organizations/366118655033/roles/myRoleFour" + } + iam_principals = { + mygroup = "group:test-group@example.com" + mysa = "serviceAccount:test@test-project.iam.gserviceaccount.com" + myuser = "user:test-user@example.com" + myuser2 = "user:test-user2@example.com" + } + locations = { + ew8 = "europe-west8" + } + tag_values = { + "test/one" = "tagValues/1234567890" + } +} +project_id = "myproject" +keyring = { + location = "$locations:ew8" + name = "test" +} +keys = { + key-a = { + iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:myuser" + ] + "roles/viewer" = [ + "$iam_principals:mysa" + ] + } + iam_by_principals = { + "$iam_principals:myuser2" = [ + "$custom_roles:myrole_three", + "$custom_roles:myrole_four", + "roles/owner", + ] + } + iam_bindings = { + myrole_two = { + role = "$custom_roles:myrole_two" + members = [ + "$iam_principals:mysa" + ] + condition = { + title = "Test" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } + } + } + key-b = { + rotation_period = "604800s" + iam_bindings_additive = { + myrole_three = { + role = "$custom_roles:myrole_three" + member = "$iam_principals:mysa" + } + } + } + key-c = { + labels = { + env = "test" + } + } +} +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:myuser" + ] + "roles/viewer" = [ + "$iam_principals:mysa" + ] +} +iam_bindings = { + myrole_two = { + role = "$custom_roles:myrole_two" + members = [ + "$iam_principals:mysa" + ] + condition = { + title = "Test" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } +} +iam_bindings_additive = { + myrole_three = { + role = "$custom_roles:myrole_three" + member = "$iam_principals:mysa" + } +} +iam_by_principals = { + "$iam_principals:myuser2" = [ + "$custom_roles:myrole_three", + "$custom_roles:myrole_four", + "roles/owner", + ] +} +tag_bindings = { + foo = "$tag_values:test/one" +} diff --git a/tests/modules/kms/context.yaml b/tests/modules/kms/context.yaml new file mode 100644 index 000000000..ce80f5190 --- /dev/null +++ b/tests/modules/kms/context.yaml @@ -0,0 +1,114 @@ +# 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_kms_crypto_key.default["key-a"]: + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + name: key-a + purpose: ENCRYPT_DECRYPT + rotation_period: null + skip_initial_version_creation: false + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + google_kms_crypto_key.default["key-b"]: + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + name: key-b + purpose: ENCRYPT_DECRYPT + rotation_period: 604800s + skip_initial_version_creation: false + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + google_kms_crypto_key.default["key-c"]: + effective_labels: + env: test + goog-terraform-provisioned: 'true' + labels: + env: test + name: key-c + purpose: ENCRYPT_DECRYPT + rotation_period: null + skip_initial_version_creation: false + terraform_labels: + env: test + goog-terraform-provisioned: 'true' + timeouts: null + google_kms_crypto_key_iam_binding.authoritative["key-a.$custom_roles:myrole_one"]: + condition: [] + members: + - user:test-user@example.com + role: organizations/366118655033/roles/myRoleOne + google_kms_crypto_key_iam_binding.authoritative["key-a.roles/viewer"]: + condition: [] + members: + - serviceAccount:test@test-project.iam.gserviceaccount.com + role: roles/viewer + google_kms_crypto_key_iam_binding.bindings["myrole_two"]: + condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + title: Test + members: + - serviceAccount:test@test-project.iam.gserviceaccount.com + role: organizations/366118655033/roles/myRoleTwo + google_kms_crypto_key_iam_member.members["myrole_three"]: + condition: [] + member: serviceAccount:test@test-project.iam.gserviceaccount.com + role: organizations/366118655033/roles/myRoleThree + google_kms_key_ring.default[0]: + location: europe-west8 + name: test + project: myproject + timeouts: null + google_kms_key_ring_iam_binding.authoritative["$custom_roles:myrole_one"]: + condition: [] + members: + - user:test-user@example.com + role: organizations/366118655033/roles/myRoleOne + google_kms_key_ring_iam_binding.authoritative["roles/viewer"]: + condition: [] + members: + - serviceAccount:test@test-project.iam.gserviceaccount.com + role: roles/viewer + google_kms_key_ring_iam_binding.bindings["myrole_two"]: + condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + title: Test + members: + - serviceAccount:test@test-project.iam.gserviceaccount.com + role: organizations/366118655033/roles/myRoleTwo + google_kms_key_ring_iam_member.bindings["myrole_three"]: + condition: [] + member: serviceAccount:test@test-project.iam.gserviceaccount.com + role: organizations/366118655033/roles/myRoleThree + google_tags_location_tag_binding.binding["foo"]: + location: europe-west8 + tag_value: tagValues/1234567890 + +counts: + google_kms_crypto_key: 3 + google_kms_crypto_key_iam_binding: 3 + google_kms_crypto_key_iam_member: 1 + google_kms_key_ring: 1 + google_kms_key_ring_iam_binding: 3 + google_kms_key_ring_iam_member: 1 + google_tags_location_tag_binding: 1 + modules: 0 + resources: 13 diff --git a/tests/modules/kms/tftest.yaml b/tests/modules/kms/tftest.yaml new file mode 100644 index 000000000..1145dd8ce --- /dev/null +++ b/tests/modules/kms/tftest.yaml @@ -0,0 +1,17 @@ +# 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module: modules/kms +tests: + context: