From 91615e014054ada63899da558f3b1c6cac5c8000 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sat, 17 Feb 2024 10:02:16 +0300 Subject: [PATCH] VPC-SC module factories (#2081) * factory untested * factory example test --- modules/vpc-sc/README.md | 108 +++++++++++++++++-- modules/vpc-sc/access-levels.tf | 2 +- modules/vpc-sc/factory.tf | 89 +++++++++++++++ modules/vpc-sc/service-perimeters-regular.tf | 21 ++-- modules/vpc-sc/variables.tf | 11 ++ tests/modules/vpc_sc/examples/factory.yaml | 106 ++++++++++++++++++ 6 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 modules/vpc-sc/factory.tf create mode 100644 tests/modules/vpc_sc/examples/factory.yaml diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index ee0d63a3..2bdeebe9 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -6,6 +6,23 @@ Given the complexity of the underlying resources, the module intentionally mimic If you are using [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) with Terraform and run into permissions issues, make sure to check out the recommended provider configuration in the [VPC SC resources documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_access_level). + +- [Examples](#examples) + - [Access policy](#access-policy) + - [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) +- [Factories](#factories) +- [Notes](#notes) +- [TODO](#todo) +- [Files](#files) +- [Variables](#variables) +- [Outputs](#outputs) + + ## Examples ### Access policy @@ -194,6 +211,83 @@ module "test" { # tftest modules=1 resources=3 inventory=regular.yaml ``` +## Factories + +This module implements support for three distinct factories, used to create and manage access levels, egress policies and ingress policies via YAML files. The YAML files syntax is a 1:1 match for the corresponding variables, and the factory data is merged at runtime with any data set in variables, which take precedence in case of key overlaps. + +This is an example that uses all three factories. Note that the factory configuration points to folders, where each file represents one resource. + +```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" + } + service_perimeters_regular = { + r1 = { + status = { + access_levels = ["geo-it", "identity-user1"] + resources = ["projects/11111", "projects/111111"] + restricted_services = ["storage.googleapis.com"] + egress_policies = ["gcs-sa-foo"] + ingress_policies = ["sa-tf-test"] + vpc_accessible_services = { + allowed_services = ["storage.googleapis.com"] + enable_restriction = true + } + } + } + } +} +# tftest modules=1 resources=3 files=a1,a2,e1,i1 inventory=factory.yaml +``` + +```yaml +conditions: + - members: + - user:user1@example.com +# tftest-file id=a1 path=data/access-levels/identity-user1.yaml +``` + +```yaml +conditions: + - regions: + - IT +# tftest-file id=a2 path=data/access-levels/geo-it.yaml +``` + +```yaml +from: + identities: + - serviceAccount:foo@myproject.iam.gserviceaccount.com +to: + operations: + - method_selectors: + - "*" + service_name: storage.googleapis.com + resources: + - projects/123456789 + +# tftest-file id=e1 path=data/egress-policies/gcs-sa-foo.yaml +``` + +```yaml +from: + access_levels: + - "*" + identities: + - serviceAccount:test-tf@myproject.iam.gserviceaccount.com +to: + operations: + - service_name: "*" + resources: + - "*" +# tftest-file id=i1 path=data/ingress-policies/sa-tf-test.yaml +``` + ## 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. @@ -209,6 +303,7 @@ module "test" { | name | description | resources | |---|---|---| | [access-levels.tf](./access-levels.tf) | Access level resources. | google_access_context_manager_access_level | +| [factory.tf](./factory.tf) | None | | | [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. | | @@ -225,12 +320,13 @@ module "test" { | [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | | [access_policy_create](variables.tf#L61) | 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#L71) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [iam](variables.tf#L102) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L108) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L123) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [ingress_policies](variables.tf#L138) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [service_perimeters_bridge](variables.tf#L170) | Bridge service perimeters. | map(object({…})) | | {} | -| [service_perimeters_regular](variables.tf#L180) | Regular service perimeters. | map(object({…})) | | {} | +| [factories_config](variables.tf#L102) | Paths to folders that enable factory functionality. | object({…}) | | {} | +| [iam](variables.tf#L113) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L119) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L134) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [ingress_policies](variables.tf#L149) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [service_perimeters_bridge](variables.tf#L181) | Bridge service perimeters. | map(object({…})) | | {} | +| [service_perimeters_regular](variables.tf#L191) | Regular service perimeters. | map(object({…})) | | {} | ## Outputs diff --git a/modules/vpc-sc/access-levels.tf b/modules/vpc-sc/access-levels.tf index 1eb85343..c00288e1 100644 --- a/modules/vpc-sc/access-levels.tf +++ b/modules/vpc-sc/access-levels.tf @@ -21,7 +21,7 @@ # google_access_context_manager_access_levels resource resource "google_access_context_manager_access_level" "basic" { - for_each = var.access_levels + for_each = merge(local.data.access_levels, var.access_levels) parent = "accessPolicies/${local.access_policy}" name = "accessPolicies/${local.access_policy}/accessLevels/${each.key}" title = each.key diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf new file mode 100644 index 00000000..eca5867a --- /dev/null +++ b/modules/vpc-sc/factory.tf @@ -0,0 +1,89 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +locals { + _data = { + for k, v in local._data_paths : k => { + for f in try(fileset(v, "**/*.yaml"), []) : + trimsuffix(f, ".yaml") => yamldecode(file("${v}/${f}")) + } + } + _data_paths = { + for k in ["access_levels", "egress_policies", "ingress_policies"] : k => ( + var.factories_config[k] == null + ? null + : pathexpand(var.factories_config[k]) + ) + } + data = { + access_levels = { + for k, v in local._data.access_levels : k => { + combining_function = try(v.combining_function, null) + description = try(v.description, null) + conditions = [ + for c in try(v.conditions, []) : merge({ + device_policy = null + ip_subnetworks = [] + members = [] + negate = null + regions = [] + required_access_levels = [] + }, c) + ] + } + } + egress_policies = { + for k, v in local._data.egress_policies : k => { + from = merge({ + identity_type = null + identities = null + }, try(v.from, {})) + to = { + operations = [ + for o in try(v.to.operations, []) : merge({ + method_selectors = [] + permission_selectors = [] + service_name = null + }, o) + ] + resources = try(v.to.resources, []) + resource_type_external = try(v.to.resource_type_external, false) + } + } + } + ingress_policies = { + for k, v in local._data.ingress_policies : k => { + from = merge({ + access_levels = [] + identity_type = null + identities = null + resources = [] + }, try(v.from, {})) + to = { + operations = [ + for o in try(v.operations, []) : merge({ + method_selectors = [] + permission_selectors = [] + service_name = null + }, o) + ] + resources = try(v.to.resources, []) + } + } + } + } + # TODO: add checks that emulate the variable validations +} diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf index 6742a1c2..a646df3c 100644 --- a/modules/vpc-sc/service-perimeters-regular.tf +++ b/modules/vpc-sc/service-perimeters-regular.tf @@ -20,6 +20,11 @@ # service perimeters are needed, switch to the # 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) +} + resource "google_access_context_manager_service_perimeter" "regular" { for_each = var.service_perimeters_regular parent = "accessPolicies/${local.access_policy}" @@ -43,8 +48,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { dynamic "egress_policies" { for_each = spec.value.egress_policies == null ? {} : { for k in spec.value.egress_policies : - k => lookup(var.egress_policies, k, null) - if contains(keys(var.egress_policies), k) + k => lookup(local.egress_policies, k, null) + if contains(keys(local.egress_policies), k) } iterator = policy content { @@ -86,8 +91,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { dynamic "ingress_policies" { for_each = spec.value.ingress_policies == null ? {} : { for k in spec.value.ingress_policies : - k => lookup(var.ingress_policies, k, null) - if contains(keys(var.ingress_policies), k) + k => lookup(local.ingress_policies, k, null) + if contains(keys(local.ingress_policies), k) } iterator = policy content { @@ -167,8 +172,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { dynamic "egress_policies" { for_each = status.value.egress_policies == null ? {} : { for k in status.value.egress_policies : - k => lookup(var.egress_policies, k, null) - if contains(keys(var.egress_policies), k) + k => lookup(local.egress_policies, k, null) + if contains(keys(local.egress_policies), k) } iterator = policy content { @@ -210,8 +215,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { dynamic "ingress_policies" { for_each = status.value.ingress_policies == null ? {} : { for k in status.value.ingress_policies : - k => lookup(var.ingress_policies, k, null) - if contains(keys(var.ingress_policies), k) + k => lookup(local.ingress_policies, k, null) + if contains(keys(local.ingress_policies), k) } iterator = policy content { diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index e852ed1c..c1c16063 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.tf @@ -99,6 +99,17 @@ variable "egress_policies" { } } +variable "factories_config" { + description = "Paths to folders that enable factory functionality." + type = object({ + access_levels = optional(string) + egress_policies = optional(string) + ingress_policies = optional(string) + }) + nullable = false + default = {} +} + variable "iam" { description = "IAM bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml new file mode 100644 index 00000000..475c4d1e --- /dev/null +++ b/tests/modules/vpc_sc/examples/factory.yaml @@ -0,0 +1,106 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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: + module.test.google_access_context_manager_access_level.basic["geo-it"]: + basic: + - combining_function: AND + conditions: + - device_policy: [] + ip_subnetworks: [] + members: [] + negate: null + regions: + - IT + required_access_levels: [] + vpc_network_sources: [] + custom: [] + description: null + name: accessPolicies/12345678/accessLevels/geo-it + parent: accessPolicies/12345678 + timeouts: null + title: geo-it + module.test.google_access_context_manager_access_level.basic["identity-user1"]: + basic: + - combining_function: AND + conditions: + - device_policy: [] + ip_subnetworks: [] + members: + - user:user1@example.com + negate: null + regions: [] + required_access_levels: [] + vpc_network_sources: [] + custom: [] + description: null + name: accessPolicies/12345678/accessLevels/identity-user1 + parent: accessPolicies/12345678 + timeouts: null + title: identity-user1 + module.test.google_access_context_manager_service_perimeter.regular["r1"]: + description: null + name: accessPolicies/12345678/servicePerimeters/r1 + parent: accessPolicies/12345678 + perimeter_type: PERIMETER_TYPE_REGULAR + spec: [] + status: + - egress_policies: + - egress_from: + - identities: + - serviceAccount:foo@myproject.iam.gserviceaccount.com + identity_type: null + source_restriction: null + sources: [] + egress_to: + - external_resources: null + operations: + - method_selectors: + - method: '*' + permission: null + service_name: storage.googleapis.com + resources: + - projects/123456789 + ingress_policies: + - ingress_from: + - identities: + - serviceAccount:test-tf@myproject.iam.gserviceaccount.com + identity_type: null + sources: + - access_level: '*' + resource: null + ingress_to: + - operations: [] + resources: + - '*' + resources: + - projects/11111 + - projects/111111 + restricted_services: + - storage.googleapis.com + vpc_accessible_services: + - allowed_services: + - storage.googleapis.com + enable_restriction: true + timeouts: null + title: r1 + use_explicit_dry_run_spec: false + +counts: + google_access_context_manager_access_level: 2 + google_access_context_manager_service_perimeter: 1 + modules: 1 + resources: 3 + +outputs: {}