From fd9f92324bccf054d2e5cffd794871fca322a1c2 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 13 Feb 2025 19:04:09 +0100 Subject: [PATCH] Update VPC-SC module and FAST stage (#2887) * Update VPC-SC module to support vpc subnets * Update FAST VPC-SC variables * Fix tests --- fast/stages/1-vpcsc/README.md | 16 +++--- fast/stages/1-vpcsc/variables.tf | 35 +++++++++++-- modules/vpc-sc/README.md | 23 ++++---- modules/vpc-sc/access-levels.tf | 13 ++++- modules/vpc-sc/factory.tf | 9 ++-- .../vpc-sc/schemas/access-level.schema.json | 12 +++++ .../vpc-sc/schemas/egress-policy.schema.json | 21 ++++++-- modules/vpc-sc/service-perimeters-regular.tf | 52 +++++++++++++++++-- modules/vpc-sc/variables.tf | 17 +++--- tests/fast/stages/s1_vpcsc/simple.yaml | 4 +- tests/modules/vpc_sc/examples/factory.yaml | 4 +- 11 files changed, 161 insertions(+), 45 deletions(-) diff --git a/fast/stages/1-vpcsc/README.md b/fast/stages/1-vpcsc/README.md index cc348c903..bd15efb7c 100644 --- a/fast/stages/1-vpcsc/README.md +++ b/fast/stages/1-vpcsc/README.md @@ -303,15 +303,15 @@ Some references that might be useful in setting up this stage: |---|---|:---:|:---:|:---:|:---:| | [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [organization](variables-fast.tf#L35) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | | -| [access_policy](variables.tf#L56) | Access policy id (used for tenant-level VPC-SC configurations). | number | | null | | -| [egress_policies](variables.tf#L62) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | -| [factories_config](variables.tf#L93) | Paths to folders that enable factory functionality. | object({…}) | | {} | | -| [ingress_policies](variables.tf#L104) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | +| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | | +| [access_policy](variables.tf#L67) | Access policy id (used for tenant-level VPC-SC configurations). | number | | null | | +| [egress_policies](variables.tf#L73) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | +| [factories_config](variables.tf#L114) | Paths to folders that enable factory functionality. | object({…}) | | {} | | +| [ingress_policies](variables.tf#L125) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | | [logging](variables-fast.tf#L25) | Log writer identities for organization / folders. | object({…}) | | null | 0-bootstrap | -| [outputs_location](variables.tf#L136) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [perimeters](variables.tf#L142) | Perimeter definitions. | map(object({…})) | | {…} | | -| [resource_discovery](variables.tf#L165) | Automatic discovery of perimeter projects. | object({…}) | | {} | | +| [outputs_location](variables.tf#L165) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [perimeters](variables.tf#L171) | Perimeter definitions. | map(object({…})) | | {…} | | +| [resource_discovery](variables.tf#L194) | Automatic discovery of perimeter projects. | object({…}) | | {} | | | [root_node](variables-fast.tf#L45) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap | ## Outputs diff --git a/fast/stages/1-vpcsc/variables.tf b/fast/stages/1-vpcsc/variables.tf index ef8307158..ca91a66a7 100644 --- a/fast/stages/1-vpcsc/variables.tf +++ b/fast/stages/1-vpcsc/variables.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. @@ -36,6 +36,7 @@ variable "access_levels" { negate = optional(bool) regions = optional(list(string), []) required_access_levels = optional(list(string), []) + vpc_subnets = optional(map(list(string)), {}) })), []) description = optional(string) })) @@ -51,6 +52,16 @@ variable "access_levels" { ]) error_message = "Invalid `combining_function` value (null, \"AND\", \"OR\" accepted)." } + validation { + condition = alltrue([ + for k, v in var.access_levels : alltrue([ + for condition in v.conditions : alltrue([ + for member in condition.members : can(regex("^(?:serviceAccount:|user:)", member)) + ]) + ]) + ]) + error_message = "Invalid `conditions[].members`. It needs to start with on of the prefixes: 'serviceAccount:' or 'user:'." + } } variable "access_policy" { @@ -63,17 +74,19 @@ variable "egress_policies" { description = "Egress policy definitions that can be referenced in perimeters." type = map(object({ from = object({ + access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) + resources = optional(list(string), []) }) to = object({ + external_resources = optional(list(string)) operations = optional(list(object({ method_selectors = optional(list(string)) permission_selectors = optional(list(string)) service_name = string })), []) - resources = optional(list(string)) - resource_type_external = optional(bool, false) + resources = optional(list(string)) }) })) default = {} @@ -88,6 +101,14 @@ variable "egress_policies" { ]) error_message = "Invalid `from.identity_type` value in egress policy." } + validation { + condition = alltrue([ + for k, v in var.egress_policies : v.from.identities == null ? true : alltrue([ + for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity)) + ]) + ]) + error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:' or 'principalSet:." + } } variable "factories_config" { @@ -131,6 +152,14 @@ variable "ingress_policies" { ]) error_message = "Invalid `from.identity_type` value in ingress policy." } + validation { + condition = alltrue([ + for k, v in var.ingress_policies : v.from.identities == null ? true : alltrue([ + for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity)) + ]) + ]) + error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:', 'principalSet:'." + } } variable "outputs_location" { diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index cafa4f627..44ede6b83 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -339,17 +339,17 @@ to: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [access_policy](variables.tf#L66) | Access Policy name, set to null if creating one. | string | ✓ | | -| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | -| [access_policy_create](variables.tf#L71) | 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#L81) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [factories_config](variables.tf#L120) | Paths to folders that enable factory functionality. | object({…}) | | {} | -| [iam](variables.tf#L132) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L138) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L153) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [ingress_policies](variables.tf#L168) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [service_perimeters_bridge](variables.tf#L208) | Bridge service perimeters. | map(object({…})) | | {} | -| [service_perimeters_regular](variables.tf#L218) | Regular service perimeters. | map(object({…})) | | {} | +| [access_policy](variables.tf#L67) | Access Policy name, set to null if creating one. | string | ✓ | | +| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | +| [access_policy_create](variables.tf#L72) | 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#L82) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [factories_config](variables.tf#L123) | Paths to folders that enable factory functionality. | object({…}) | | {} | +| [iam](variables.tf#L135) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L141) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L156) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [ingress_policies](variables.tf#L171) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [service_perimeters_bridge](variables.tf#L211) | Bridge service perimeters. | map(object({…})) | | {} | +| [service_perimeters_regular](variables.tf#L221) | Regular service perimeters. | map(object({…})) | | {} | ## Outputs @@ -363,7 +363,6 @@ to: | [service_perimeters_bridge](outputs.tf#L47) | Bridge service perimeter resources. | | | [service_perimeters_regular](outputs.tf#L52) | Regular service perimeter resources. | | - ## Tests ```hcl diff --git a/modules/vpc-sc/access-levels.tf b/modules/vpc-sc/access-levels.tf index c00288e16..b173f0f60 100644 --- a/modules/vpc-sc/access-levels.tf +++ b/modules/vpc-sc/access-levels.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. @@ -72,8 +72,17 @@ resource "google_access_context_manager_access_level" "basic" { } } + dynamic "vpc_network_sources" { + for_each = c.value.vpc_subnets + iterator = vpc + content { + vpc_subnetwork { + network = vpc.key + vpc_ip_subnetworks = vpc.value + } + } + } } } - } } diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf index 4e9cdc740..eb4608364 100644 --- a/modules/vpc-sc/factory.tf +++ b/modules/vpc-sc/factory.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. @@ -41,6 +41,7 @@ locals { negate = null regions = [] required_access_levels = [] + vpc_subnets = {} }, c) ] } @@ -48,10 +49,13 @@ locals { egress_policies = { for k, v in local._data.egress_policies : k => { from = merge({ + access_levels = [] identity_type = null identities = [] + resources = [] }, try(v.from, {})) to = { + external_resources = try(v.to.external_resources, null) operations = [ for o in try(v.to.operations, []) : merge({ method_selectors = [] @@ -59,8 +63,7 @@ locals { service_name = null }, o) ] - resources = try(v.to.resources, []) - resource_type_external = try(v.to.resource_type_external, false) + resources = try(v.to.resources, []) } } } diff --git a/modules/vpc-sc/schemas/access-level.schema.json b/modules/vpc-sc/schemas/access-level.schema.json index 7945559cd..664f84bf3 100644 --- a/modules/vpc-sc/schemas/access-level.schema.json +++ b/modules/vpc-sc/schemas/access-level.schema.json @@ -92,6 +92,18 @@ "items": { "type": "string" } + }, + "vpc_subnets": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^//compute.googleapis.com/projects/[^/]+/global/networks/[^/]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } diff --git a/modules/vpc-sc/schemas/egress-policy.schema.json b/modules/vpc-sc/schemas/egress-policy.schema.json index c8e0931bd..316c9d588 100644 --- a/modules/vpc-sc/schemas/egress-policy.schema.json +++ b/modules/vpc-sc/schemas/egress-policy.schema.json @@ -12,6 +12,12 @@ "type": "object", "additionalProperties": false, "properties": { + "access_levels": { + "type": "array", + "items": { + "type": "string" + } + }, "identity_type": { "enum": [ "IDENTITY_TYPE_UNSPECIFIED", @@ -27,6 +33,12 @@ "type": "string", "pattern": "^(?:serviceAccount:|user:|group:|principal:)" } + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -34,6 +46,12 @@ "type": "object", "additionalProperties": false, "properties": { + "external_resources": { + "type": "array", + "items": { + "type": "string" + } + }, "operations": { "type": "array", "items": { @@ -66,9 +84,6 @@ "items": { "type": "string" } - }, - "resource_type_external": { - "type": "boolean" } } } diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf index 3bc1744ff..970d49bc3 100644 --- a/modules/vpc-sc/service-perimeters-regular.tf +++ b/modules/vpc-sc/service-perimeters-regular.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. @@ -59,12 +59,35 @@ resource "google_access_context_manager_service_perimeter" "regular" { content { identity_type = policy.value.from.identity_type identities = policy.value.from.identities + source_restriction = ( + length(policy.value.from.access_levels) > 0 || length(policy.value.from.resources) > 0 + ? "SOURCE_RESTRICTION_ENABLED" + : "SOURCE_RESTRICTION_DISABLED" + ) + dynamic "sources" { + for_each = policy.value.from.access_levels + iterator = access_level + content { + access_level = try( + google_access_context_manager_access_level.basic[access_level.value].id, + access_level.value + ) + } + } + dynamic "sources" { + for_each = policy.value.from.resources + iterator = resource + content { + resource = resource.value + } + } } } dynamic "egress_to" { for_each = policy.value.to == null ? [] : [""] content { - resources = policy.value.to.resources + external_resources = policy.value.to.external_resources + resources = policy.value.to.resources dynamic "operations" { for_each = toset(policy.value.to.operations) iterator = o @@ -183,12 +206,35 @@ resource "google_access_context_manager_service_perimeter" "regular" { content { identity_type = policy.value.from.identity_type identities = policy.value.from.identities + source_restriction = ( + length(policy.value.from.access_levels) > 0 || length(policy.value.from.resources) > 0 + ? "SOURCE_RESTRICTION_ENABLED" + : "SOURCE_RESTRICTION_DISABLED" + ) + dynamic "sources" { + for_each = policy.value.from.access_levels + iterator = access_level + content { + access_level = try( + google_access_context_manager_access_level.basic[access_level.value].id, + access_level.value + ) + } + } + dynamic "sources" { + for_each = policy.value.from.resources + iterator = resource + content { + resource = resource.value + } + } } } dynamic "egress_to" { for_each = policy.value.to == null ? [] : [""] content { - resources = policy.value.to.resources + external_resources = policy.value.to.external_resources + resources = policy.value.to.resources dynamic "operations" { for_each = toset(policy.value.to.operations) iterator = o diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index 7a1304921..baf08fadb 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.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. @@ -36,6 +36,7 @@ variable "access_levels" { negate = optional(bool) regions = optional(list(string), []) required_access_levels = optional(list(string), []) + vpc_subnets = optional(map(list(string)), {}) })), []) description = optional(string) })) @@ -82,17 +83,19 @@ variable "egress_policies" { description = "Egress policy definitions that can be referenced in perimeters." type = map(object({ from = object({ + access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) + resources = optional(list(string), []) }) to = object({ + external_resources = optional(list(string)) operations = optional(list(object({ method_selectors = optional(list(string)) permission_selectors = optional(list(string)) service_name = string })), []) - resources = optional(list(string)) - resource_type_external = optional(bool, false) + resources = optional(list(string)) }) })) default = {} @@ -110,10 +113,10 @@ variable "egress_policies" { validation { condition = alltrue([ for k, v in var.egress_policies : v.from.identities == null ? true : alltrue([ - for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:)", identity)) + for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity)) ]) ]) - error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:' or 'principal:'." + error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:' or 'principalSet:." } } @@ -198,10 +201,10 @@ variable "ingress_policies" { validation { condition = alltrue([ for k, v in var.ingress_policies : v.from.identities == null ? true : alltrue([ - for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:)", identity)) + for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity)) ]) ]) - error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:' or 'principal:'." + error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:', 'principalSet:'." } } diff --git a/tests/fast/stages/s1_vpcsc/simple.yaml b/tests/fast/stages/s1_vpcsc/simple.yaml index 3350d3dc9..819c2b965 100644 --- a/tests/fast/stages/s1_vpcsc/simple.yaml +++ b/tests/fast/stages/s1_vpcsc/simple.yaml @@ -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. @@ -74,7 +74,7 @@ values: - identities: - user:user@fast.example.com identity_type: null - source_restriction: null + source_restriction: SOURCE_RESTRICTION_DISABLED sources: [] egress_to: - external_resources: null diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml index 007eebf9c..6159adcd6 100644 --- a/tests/modules/vpc_sc/examples/factory.yaml +++ b/tests/modules/vpc_sc/examples/factory.yaml @@ -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. @@ -62,7 +62,7 @@ values: - serviceAccount:bar@myproject.iam.gserviceaccount.com - serviceAccount:foo@myproject.iam.gserviceaccount.com identity_type: null - source_restriction: null + source_restriction: 'SOURCE_RESTRICTION_DISABLED' sources: [] egress_to: - external_resources: null