From 7ceb814986d02a8d1e2167d6c892bcb752962d05 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Fri, 9 May 2025 14:37:03 +0200 Subject: [PATCH] VPC SC module refactor (#3062) * Remove bridge perimeters * Update FAST stages * Allow project ids in perimeter definitions * Preserve order order for ingress/egress policies * Use CAI * Use CAI * Fix tests --- .../data-solutions/shielded-folder/main.tf | 4 +- .../serverless/cloud-run-corporate/main.tf | 4 +- fast/stages/1-vpcsc/README.md | 4 +- fast/stages/1-vpcsc/main.tf | 5 +- fast/stages/1-vpcsc/outputs.tf | 8 +- modules/vpc-sc/README.md | 223 +++++++++--------- modules/vpc-sc/factory.tf | 12 +- modules/vpc-sc/main.tf | 54 ++++- modules/vpc-sc/outputs.tf | 9 +- ...ce-perimeters-regular.tf => perimeters.tf} | 98 ++++---- modules/vpc-sc/service-perimeters-bridge.tf | 64 ----- modules/vpc-sc/variables.tf | 21 +- tests/modules/vpc_sc/examples/factory.yaml | 34 +-- tests/modules/vpc_sc/examples/regular.yaml | 32 +-- 14 files changed, 276 insertions(+), 296 deletions(-) rename modules/vpc-sc/{service-perimeters-regular.tf => perimeters.tf} (82%) delete mode 100644 modules/vpc-sc/service-perimeters-bridge.tf diff --git a/blueprints/data-solutions/shielded-folder/main.tf b/blueprints/data-solutions/shielded-folder/main.tf index 472bfc393..3e1830398 100644 --- a/blueprints/data-solutions/shielded-folder/main.tf +++ b/blueprints/data-solutions/shielded-folder/main.tf @@ -1,4 +1,4 @@ -# 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. @@ -125,7 +125,7 @@ module "vpc-sc" { access_levels = var.vpc_sc_access_levels egress_policies = var.vpc_sc_egress_policies ingress_policies = merge(var.vpc_sc_ingress_policies, local._sink_ingress_policies) - service_perimeters_regular = { + perimeters = { shielded = { # Move `spec` definition to `status` and comment `use_explicit_dry_run_spec` variable to enforce VPC-SC configuration # Before enforcing configuration check logs and create Access Level, Ingress/Egress policy as needed diff --git a/blueprints/serverless/cloud-run-corporate/main.tf b/blueprints/serverless/cloud-run-corporate/main.tf index cc3b44b15..30ca204e0 100644 --- a/blueprints/serverless/cloud-run-corporate/main.tf +++ b/blueprints/serverless/cloud-run-corporate/main.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. @@ -569,7 +569,7 @@ module "vpc_sc" { } } } - service_perimeters_regular = { + perimeters = { cloudrun = { status = { resources = [ diff --git a/fast/stages/1-vpcsc/README.md b/fast/stages/1-vpcsc/README.md index 22bccff72..8befaace3 100644 --- a/fast/stages/1-vpcsc/README.md +++ b/fast/stages/1-vpcsc/README.md @@ -318,6 +318,6 @@ Some references that might be useful in setting up this stage: | name | description | sensitive | consumers | |---|---|:---:|---| -| [tfvars](outputs.tf#L43) | Terraform variable files for the following stages. | ✓ | | -| [vpc_sc_perimeter_default](outputs.tf#L49) | Raw default perimeter resource. | ✓ | | +| [tfvars](outputs.tf#L39) | Terraform variable files for the following stages. | ✓ | | +| [vpc_sc_perimeter_default](outputs.tf#L45) | Raw default perimeter resource. | ✓ | | diff --git a/fast/stages/1-vpcsc/main.tf b/fast/stages/1-vpcsc/main.tf index fb994b920..827c69fee 100644 --- a/fast/stages/1-vpcsc/main.tf +++ b/fast/stages/1-vpcsc/main.tf @@ -59,6 +59,7 @@ module "vpc-sc" { context = local.context } ) - ingress_policies = var.ingress_policies - service_perimeters_regular = var.perimeters + ingress_policies = var.ingress_policies + perimeters = var.perimeters + project_id_search_scope = "organizations/${var.organization.id}" } diff --git a/fast/stages/1-vpcsc/outputs.tf b/fast/stages/1-vpcsc/outputs.tf index 3866930b1..aa139c588 100644 --- a/fast/stages/1-vpcsc/outputs.tf +++ b/fast/stages/1-vpcsc/outputs.tf @@ -17,11 +17,7 @@ locals { tfvars = { perimeters = { - for k, v in try(module.vpc-sc.service_perimeters_regular, {}) : - k => v.id - } - perimeters_bridge = { - for k, v in try(module.vpc-sc.service_perimeters_bridge, {}) : + for k, v in try(module.vpc-sc.perimeters, {}) : k => v.id } } @@ -49,5 +45,5 @@ output "tfvars" { output "vpc_sc_perimeter_default" { description = "Raw default perimeter resource." sensitive = true - value = try(module.vpc-sc.service_perimeters_regular["default"], null) + value = try(module.vpc-sc.perimeters["default"], null) } diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index 49118abdc..31704e0a1 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -12,12 +12,10 @@ If you are using [Application Default Credentials](https://cloud.google.com/sdk/ - [Scoped policy](#scoped-policy) - [Access policy IAM](#access-policy-iam) - [Access levels](#access-levels) - - [Service perimeters](#service-perimeters) - - [Bridge type](#bridge-type) - - [Regular type](#regular-type) + - [Perimeters](#perimeters) +- [Automatic Project ID to Project Number Conversion](#automatic-project-id-to-project-number-conversion) - [Factories](#factories) - [Notes](#notes) -- [TODO](#todo) - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) @@ -112,36 +110,7 @@ module "test" { # tftest modules=1 resources=2 inventory=access-levels.yaml ``` -### Service perimeters - -Bridge and regular service perimeters use two separate variables, as bridge perimeters only accept a limited number of arguments, and can leverage a much simpler interface. - -The regular perimeters variable exposes all the complexity of the underlying resource, use [its documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter) as a reference about the possible values and configurations. - -If you need to refer to access levels created by the same module in regular service perimeters, you can either use the module's outputs in the provided variables, or the key used to identify the relevant access level. The example below shows how to do this in practice. - -If you are managing perimeter membership outside of this module via `google_access_context_manager_service_perimeter_resource`, for example at project creation in a project factory, you might want to uncomment the lifecycle blocks that are defined but currently unused in `service-perimeters-regular.tf` and `service-perimeters-bridge.tf`. - -#### Bridge type - -```hcl -module "test" { - source = "./fabric/modules/vpc-sc" - access_policy = "12345678" - service_perimeters_bridge = { - b1 = { - status_resources = ["projects/111110", "projects/111111"] - } - b2 = { - spec_resources = ["projects/222220", "projects/222221"] - use_explicit_dry_run_spec = true - } - } -} -# tftest modules=1 resources=2 inventory=bridge.yaml -``` - -#### Regular type +### Perimeters ```hcl module "test" { @@ -205,7 +174,7 @@ module "test" { } } } - service_perimeters_regular = { + perimeters = { r1 = { status = { access_levels = ["a1", "a2"] @@ -224,13 +193,56 @@ module "test" { # tftest modules=1 resources=3 inventory=regular.yaml ``` +## Automatic Project ID to Project Number Conversion + +As a convenience, this module can optionally convert project IDs to project numbers. Set `var.project_id_search_scope` to a folder or organization ID to define the search scope. + +The caller must have `cloudasset.assets.searchAllResources` permission to perform the search. Roles like `roles/accesscontextmanager.policyAdmin`, `roles/cloudasset.viewer`, or `roles/viewer` grant this. + +```hcl +module "vpc-sc" { + source = "./fabric/modules/vpc-sc" + project_id_search_scope = var.org_id + + access_policy = "12345678" + ingress_policies = { + i1 = { + from = { + identities = [ + "serviceAccount:foo@myproject.iam.gserviceaccount.com" + ] + resources = ["projects/my-source-project"] + } + to = { + operations = [{ + method_selectors = ["*"] + service_name = "storage.googleapis.com" + }] + resources = ["projects/my-destionation-project"] + } + } + } + + perimeters = { + p = { + spec = { + ingress_policies = ["i1"] + resources = ["projects/my-destionation-project"] + } + use_explicit_dry_run_spec = true + } + } +} +# tftest skip because uses data sources +``` + ## Factories -This module implements support for five distinct factories, used to create and manage perimeters, bridges, access levels, egress policies, and ingress policies via YAML files. +This module implements support for four distinct factories, used to create and manage perimeters, access levels, egress policies, and ingress policies via YAML files. JSON Schema files for each factory object are available in the [`schemas`](./schemas/) folder, and can be used to validate input YAML data with [`validate-yaml`](https://github.com/gerald1248/validate-yaml) or any of the available tools and libraries. -This is an example that uses only three factories and leave perimeter management in tfvars. Note that the factory configuration points to folders, where each file represents one resource. +3Note that the factory configuration points to folders, where each file represents one resource. ```hcl module "test" { @@ -240,30 +252,62 @@ module "test" { access_levels = "data/access-levels" egress_policies = "data/egress-policies" ingress_policies = "data/ingress-policies" + perimeters = "data/perimeters" context = { resource_sets = { foo_projects = ["projects/321", "projects/654"] } } } - service_perimeters_regular = { - perimeter-north = { - description = "Main perimeter" - status = { - access_levels = ["geo-it", "identity-user1"] - resources = ["projects/1111", "projects/2222"] - restricted_services = ["storage.googleapis.com"] - egress_policies = ["gcs-sa-foo"] - ingress_policies = ["sa-tf-test-geo", "sa-tf-test"] - vpc_accessible_services = { - allowed_services = ["storage.googleapis.com"] - enable_restriction = true - } - } - } - } } -# tftest modules=1 resources=3 files=a1,a2,e1,i1,i2 inventory=factory.yaml +# tftest modules=1 resources=3 files=p1,a1,a2,e1,i1,i2 inventory=factory.yaml +``` + +```yaml +description: Main perimeter +status: + access_levels: + - "geo-it" + - "identity-user1" + resources: + - "projects/1111" + - "projects/2222" + restricted_services: + - "storage.googleapis.com" + egress_policies: + - "gcs-sa-foo" + ingress_policies: + - "sa-tf-test-geo" + - "sa-tf-test" + vpc_accessible_services: + allowed_services: + - "storage.googleapis.com" + enable_restriction: yes + +# tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json +``` + +```yaml +description: "Main perimeter" +status: + access_levels: + - geo-it + - identity-user1 + resources: + - projects/1111 + - projects/2222 + restricted_services: + - storage.googleapis.com + egress_policies: + - gcs-sa-foo + ingress_policies: + - sa-tf-test-geo + - sa-tf-test + vpc_accessible_services: + allowed_services: + - storage.googleapis.com + enable_restriction: true +# tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json ``` ```yaml @@ -329,59 +373,10 @@ to: # tftest-file id=i2 path=data/ingress-policies/sa-tf-test-geo.yaml schema=ingress-policy.schema.json ``` -But perimeters could also defined in a yaml file. - -```hcl -module "test" { - source = "./fabric/modules/vpc-sc" - access_policy = "12345678" - factories_config = { - access_levels = "data/access-levels" - egress_policies = "data/egress-policies" - ingress_policies = "data/ingress-policies" - perimeters = "data/perimeters" - context = { - resource_sets = { - foo_projects = ["projects/321", "projects/654"] - } - } - } -} -# tftest modules=1 resources=3 files=a1,a2,e1,i1,i2,r1 inventory=factory.yaml -``` - -```yaml -description: Main perimeter -status: - access_levels: - - "geo-it" - - "identity-user1" - resources: - - "projects/1111" - - "projects/2222" - restricted_services: - - "storage.googleapis.com" - egress_policies: - - "gcs-sa-foo" - ingress_policies: - - "sa-tf-test-geo" - - "sa-tf-test" - vpc_accessible_services: - allowed_services: - - "storage.googleapis.com" - enable_restriction: yes - -# tftest-file id=r1 path=data/perimeters/perimeter-north.yaml schema=perimeters.schema.json -```` - ## Notes - To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again. -## TODO - -- [ ] implement support for the `google_access_context_manager_gcp_user_access_binding` resource - ## Files @@ -393,8 +388,7 @@ status: | [iam.tf](./iam.tf) | IAM bindings | google_access_context_manager_access_policy_iam_binding · google_access_context_manager_access_policy_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_access_context_manager_access_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | -| [service-perimeters-bridge.tf](./service-perimeters-bridge.tf) | Bridge service perimeter resources. | google_access_context_manager_service_perimeter | -| [service-perimeters-regular.tf](./service-perimeters-regular.tf) | Regular service perimeter resources. | google_access_context_manager_service_perimeter | +| [perimeters.tf](./perimeters.tf) | Regular service perimeter resources. | google_access_context_manager_service_perimeter | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -406,13 +400,13 @@ status: | [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | | [access_policy_create](variables.tf#L73) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format. | object({…}) | | null | | [egress_policies](variables.tf#L83) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [factories_config](variables.tf#L126) | Paths to folders that enable factory functionality. | object({…}) | | {} | -| [iam](variables.tf#L144) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L150) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L165) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [ingress_policies](variables.tf#L180) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [service_perimeters_bridge](variables.tf#L222) | Bridge service perimeters. | map(object({…})) | | {} | -| [service_perimeters_regular](variables.tf#L234) | Regular service perimeters. | map(object({…})) | | {} | +| [factories_config](variables.tf#L126) | Paths to folders that enable factory functionality. | object({…}) | | {} | +| [iam](variables.tf#L143) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L149) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L164) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [ingress_policies](variables.tf#L179) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [perimeters](variables.tf#L221) | Regular service perimeters. | map(object({…})) | | {} | +| [project_id_search_scope](variables.tf#L254) | Set this to an organization or folder ID to use Cloud Asset Inventory to automatically translate project ids to numbers. | string | | null | ## Outputs @@ -423,8 +417,7 @@ status: | [access_policy](outputs.tf#L30) | Access policy resource, if autocreated. | | | [access_policy_name](outputs.tf#L37) | Access policy name. | | | [id](outputs.tf#L42) | Fully qualified access policy id. | | -| [service_perimeters_bridge](outputs.tf#L47) | Bridge service perimeter resources. | | -| [service_perimeters_regular](outputs.tf#L52) | Regular service perimeter resources. | | +| [perimeters](outputs.tf#L47) | Regular service perimeter resources. | | ## Tests @@ -451,7 +444,7 @@ module "test" { } } } - service_perimeters_regular = { + perimeters = { default = { status = { access_levels = ["geo-it"] diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf index 824b52793..962b0ac45 100644 --- a/modules/vpc-sc/factory.tf +++ b/modules/vpc-sc/factory.tf @@ -22,7 +22,7 @@ locals { } } _data_paths = { - for k in ["access_levels", "bridges", "egress_policies", "ingress_policies", "perimeters"] : k => ( + for k in ["access_levels", "egress_policies", "ingress_policies", "perimeters"] : k => ( var.factories_config[k] == null ? null : pathexpand(var.factories_config[k]) @@ -92,16 +92,6 @@ locals { } } } - bridges = { - for k, v in local._data.bridges : - k => { - description = try(v.description, null) - title = try(v.title, null) - spec_resources = try(v.spec_resources, null) - status_resources = try(v.resources, null) - use_explicit_dry_run_spec = try(v.use_explicit_dry_run_spec, false) - } - } perimeters = { for k, v in local._data.perimeters : k => { diff --git a/modules/vpc-sc/main.tf b/modules/vpc-sc/main.tf index 89434d05c..e764ab8b8 100644 --- a/modules/vpc-sc/main.tf +++ b/modules/vpc-sc/main.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. @@ -19,6 +19,49 @@ locals { google_access_context_manager_access_policy.default[0].name, var.access_policy ) + cai_query = join(" OR ", + formatlist( + "\"//cloudresourcemanager.googleapis.com/projects/%s\"", + local._project_ids + ) + ) + do_cai_query = ( + var.project_id_search_scope == null + ? false + : length(local._project_ids) > 0 + ) + + # collect project ids and convert them to numbers + _all_project_identifiers = distinct(flatten([ + for k, v in local.perimeters : [ + try(v.status.resources, []), + try(v.spec.resources, []), + [ + for _, vv in local.ingress_policies : [ + try(vv.from.resources, []), + try(vv.to.resources, []) + ] + ], + [ + for _, vv in local.egress_policies : [ + try(vv.from.resources, []), + try(vv.to.resources, []) + ] + ], + ] + ])) + _project_ids = [ + for x in local._all_project_identifiers : + trimprefix(x, "projects/") + if can(regex("^projects/[a-z]", x)) + ] + project_number = (local.do_cai_query + ? { + for x in data.google_cloud_asset_search_all_resources.projects[0].results : + (trimprefix(x.name, "//cloudresourcemanager.googleapis.com/")) => x.project + } + : {} + ) } resource "google_access_context_manager_access_policy" "default" { @@ -27,3 +70,12 @@ resource "google_access_context_manager_access_policy" "default" { title = var.access_policy_create.title scopes = var.access_policy_create.scopes } + +data "google_cloud_asset_search_all_resources" "projects" { + count = local.do_cai_query ? 1 : 0 + scope = var.project_id_search_scope + asset_types = [ + "cloudresourcemanager.googleapis.com/Project" + ] + query = "name=${local.cai_query}" +} diff --git a/modules/vpc-sc/outputs.tf b/modules/vpc-sc/outputs.tf index 280ba4410..00f8b76d2 100644 --- a/modules/vpc-sc/outputs.tf +++ b/modules/vpc-sc/outputs.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 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. @@ -44,12 +44,7 @@ output "id" { value = local.access_policy } -output "service_perimeters_bridge" { - description = "Bridge service perimeter resources." - value = google_access_context_manager_service_perimeter.bridge -} - -output "service_perimeters_regular" { +output "perimeters" { description = "Regular service perimeter resources." value = google_access_context_manager_service_perimeter.regular } diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/perimeters.tf similarity index 82% rename from modules/vpc-sc/service-perimeters-regular.tf rename to modules/vpc-sc/perimeters.tf index 120e6939b..326a0db31 100644 --- a/modules/vpc-sc/service-perimeters-regular.tf +++ b/modules/vpc-sc/perimeters.tf @@ -21,13 +21,13 @@ # google_access_context_manager_service_perimeters resource locals { - egress_policies = merge(local.data.egress_policies, var.egress_policies) - ingress_policies = merge(local.data.ingress_policies, var.ingress_policies) - regular_perimeters = merge(local.data.perimeters, var.service_perimeters_regular) + egress_policies = merge(local.data.egress_policies, var.egress_policies) + ingress_policies = merge(local.data.ingress_policies, var.ingress_policies) + perimeters = merge(local.data.perimeters, var.perimeters) } resource "google_access_context_manager_service_perimeter" "regular" { - for_each = local.regular_perimeters + for_each = local.perimeters parent = "accessPolicies/${local.access_policy}" name = "accessPolicies/${local.access_policy}/servicePerimeters/${each.key}" description = each.value.description @@ -45,8 +45,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { ] ) resources = flatten([ - for r in spec.value.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in spec.value.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) restricted_services = flatten([ for r in coalesce(spec.value.restricted_services, []) : @@ -54,13 +56,13 @@ resource "google_access_context_manager_service_perimeter" "regular" { ]) dynamic "egress_policies" { - for_each = spec.value.egress_policies == null ? {} : { + for_each = spec.value.egress_policies == null ? [] : [ for k in spec.value.egress_policies : - k => local.egress_policies[k] - } + merge(local.egress_policies[k], { key = k }) + ] iterator = policy content { - title = coalesce(policy.value.title, policy.key) + title = coalesce(policy.value.title, policy.value.key) dynamic "egress_from" { for_each = policy.value.from == null ? [] : [""] content { @@ -86,8 +88,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "sources" { for_each = flatten([ - for r in policy.value.from.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.from.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) iterator = resource content { @@ -101,8 +105,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { content { external_resources = policy.value.to.external_resources resources = flatten([ - for r in policy.value.to.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.to.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) roles = policy.value.to.roles @@ -131,13 +137,13 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "ingress_policies" { - for_each = spec.value.ingress_policies == null ? {} : { + for_each = spec.value.ingress_policies == null ? [] : [ for k in spec.value.ingress_policies : - k => local.ingress_policies[k] - } + merge(local.ingress_policies[k], { key = k }) + ] iterator = policy content { - title = coalesce(policy.value.title, policy.key) + title = coalesce(policy.value.title, policy.value.key) dynamic "ingress_from" { for_each = policy.value.from == null ? [] : [""] content { @@ -157,8 +163,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "sources" { for_each = flatten([ - for r in policy.value.from.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.from.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) content { resource = sources.value @@ -170,8 +178,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { for_each = policy.value.to == null ? [] : [""] content { resources = flatten([ - for r in policy.value.to.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.to.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) roles = policy.value.to.roles dynamic "operations" { @@ -199,7 +209,7 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "vpc_accessible_services" { - for_each = spec.value.vpc_accessible_services == null ? {} : { 1 = 1 } + for_each = spec.value.vpc_accessible_services == null ? [] : [""] content { allowed_services = flatten([ for r in spec.value.vpc_accessible_services.allowed_services : @@ -222,8 +232,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { ] ) resources = flatten([ - for r in status.value.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in status.value.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) restricted_services = flatten([ for r in coalesce(status.value.restricted_services, []) : @@ -231,13 +243,13 @@ resource "google_access_context_manager_service_perimeter" "regular" { ]) dynamic "egress_policies" { - for_each = status.value.egress_policies == null ? {} : { + for_each = status.value.egress_policies == null ? [] : [ for k in status.value.egress_policies : - k => local.egress_policies[k] - } + merge(local.egress_policies[k], { key = k }) + ] iterator = policy content { - title = coalesce(policy.value.title, policy.key) + title = coalesce(policy.value.title, policy.value.key) dynamic "egress_from" { for_each = policy.value.from == null ? [] : [""] content { @@ -263,8 +275,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "sources" { for_each = flatten([ - for r in policy.value.from.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.from.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) iterator = resource content { @@ -304,13 +318,13 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "ingress_policies" { - for_each = status.value.ingress_policies == null ? {} : { + for_each = status.value.ingress_policies == null ? [] : [ for k in status.value.ingress_policies : - k => local.ingress_policies[k] - } + merge(local.ingress_policies[k], { key = k }) + ] iterator = policy content { - title = coalesce(policy.value.title, policy.key) + title = coalesce(policy.value.title, policy.value.key) dynamic "ingress_from" { for_each = policy.value.from == null ? [] : [""] content { @@ -331,8 +345,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "sources" { for_each = flatten([ - for r in policy.value.from.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.from.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) content { resource = sources.value @@ -344,8 +360,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { for_each = policy.value.to == null ? [] : [""] content { resources = flatten([ - for r in policy.value.to.resources : - lookup(var.factories_config.context.resource_sets, r, [r]) + for r in policy.value.to.resources : try( + var.factories_config.context.resource_sets[r], + [local.project_number[r]], [r] + ) ]) roles = policy.value.to.roles dynamic "operations" { @@ -373,7 +391,7 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "vpc_accessible_services" { - for_each = status.value.vpc_accessible_services == null ? {} : { 1 = 1 } + for_each = status.value.vpc_accessible_services == null ? [] : [""] content { allowed_services = flatten([ for r in status.value.vpc_accessible_services.allowed_services : diff --git a/modules/vpc-sc/service-perimeters-bridge.tf b/modules/vpc-sc/service-perimeters-bridge.tf deleted file mode 100644 index c826a9dc3..000000000 --- a/modules/vpc-sc/service-perimeters-bridge.tf +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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. - */ - -# tfdoc:file:description Bridge service perimeter resources. - -# this code implements "additive" service perimeters, if "authoritative" -# service perimeters are needed, switch to the -# google_access_context_manager_service_perimeters resource - -locals { - bridge_perimeters = merge(local.data.bridges, var.service_perimeters_bridge) -} - -resource "google_access_context_manager_service_perimeter" "bridge" { - for_each = local.bridge_perimeters - parent = "accessPolicies/${local.access_policy}" - name = "accessPolicies/${local.access_policy}/servicePerimeters/${each.key}" - title = each.key - perimeter_type = "PERIMETER_TYPE_BRIDGE" - use_explicit_dry_run_spec = each.value.use_explicit_dry_run_spec - - dynamic "spec" { - for_each = each.value.spec_resources == null ? [] : [""] - content { - resources = flatten([ - for r in each.value.spec_resources : - lookup(var.factories_config.context.resource_sets, r, [r]) - ]) - } - } - - dynamic "status" { - for_each = each.value.status_resources == null ? [] : [""] - content { - resources = flatten([ - for r in each.value.status_resources : - lookup(var.factories_config.context.resource_sets, r, [r]) - ]) - } - } - - # lifecycle { - # ignore_changes = [spec[0].resources, status[0].resources] - # } - - depends_on = [ - google_access_context_manager_access_policy.default, - google_access_context_manager_access_level.basic, - google_access_context_manager_service_perimeter.regular - ] -} diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index 6710a202b..e663adc70 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.tf @@ -127,7 +127,6 @@ variable "factories_config" { description = "Paths to folders that enable factory functionality." type = object({ access_levels = optional(string) - bridges = optional(string) egress_policies = optional(string) ingress_policies = optional(string) perimeters = optional(string) @@ -219,19 +218,7 @@ variable "ingress_policies" { } } -variable "service_perimeters_bridge" { - description = "Bridge service perimeters." - type = map(object({ - description = optional(string) - title = optional(string) - spec_resources = optional(list(string)) - status_resources = optional(list(string)) - use_explicit_dry_run_spec = optional(bool, false) - })) - default = {} -} - -variable "service_perimeters_regular" { +variable "perimeters" { description = "Regular service perimeters." type = map(object({ description = optional(string) @@ -263,3 +250,9 @@ variable "service_perimeters_regular" { default = {} nullable = false } + +variable "project_id_search_scope" { + description = "Set this to an organization or folder ID to use Cloud Asset Inventory to automatically translate project ids to numbers." + type = string + default = null +} diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml index a38df16d0..a258e5249 100644 --- a/tests/modules/vpc_sc/examples/factory.yaml +++ b/tests/modules/vpc_sc/examples/factory.yaml @@ -73,7 +73,25 @@ values: service_name: storage.googleapis.com resources: - projects/123456789 + roles: [] + title: gcs-sa-foo ingress_policies: + - ingress_from: + - identities: + - serviceAccount:test-tf@myproject.iam.gserviceaccount.com + identity_type: null + sources: + - resource: null + ingress_to: + - operations: + - method_selectors: [] + service_name: '*' + resources: + - projects/1234567890 + - projects/321 + - projects/654 + roles: [] + title: sa-tf-test-geo - ingress_from: - identities: - serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com @@ -92,20 +110,8 @@ values: service_name: compute.googleapis.com resources: - '*' - - ingress_from: - - identities: - - serviceAccount:test-tf@myproject.iam.gserviceaccount.com - identity_type: null - sources: - - resource: null - ingress_to: - - operations: - - method_selectors: [] - service_name: '*' - resources: - - projects/1234567890 - - projects/321 - - projects/654 + roles: [] + title: sa-tf-test resources: - projects/1111 - projects/2222 diff --git a/tests/modules/vpc_sc/examples/regular.yaml b/tests/modules/vpc_sc/examples/regular.yaml index e84dea712..f97121802 100644 --- a/tests/modules/vpc_sc/examples/regular.yaml +++ b/tests/modules/vpc_sc/examples/regular.yaml @@ -75,22 +75,6 @@ values: roles: null title: gcs-sa-foo ingress_policies: - - ingress_from: - - identities: - - serviceAccount:test-tf-2@myproject.iam.gserviceaccount.com - identity_type: null - sources: - - access_level: '*' - resource: null - ingress_to: - - operations: - - method_selectors: [] - service_name: '*' - resources: - - '*' - roles: - - roles/storage.objectViewer - title: sa-roles - ingress_from: - identities: - serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com @@ -107,6 +91,22 @@ values: - '*' roles: null title: sa-tf-test + - ingress_from: + - identities: + - serviceAccount:test-tf-2@myproject.iam.gserviceaccount.com + identity_type: null + sources: + - access_level: '*' + resource: null + ingress_to: + - operations: + - method_selectors: [] + service_name: '*' + resources: + - '*' + roles: + - roles/storage.objectViewer + title: sa-roles resources: - projects/1111 - projects/2222