From 2c7dab3bb2b572c4d60bc91ba48eb9e4ffbb8913 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 31 Dec 2021 13:29:22 +0100 Subject: [PATCH] New vpc-sc module implementation (#406) * first implementation * minimal output * split service perimeters in regular and bridge * tests and fixes * new vpc-sc implementation * remove providers file used for testing * remove provider used during development --- modules/vpc-sc/README.md | 378 +++++++----------- modules/vpc-sc/access_levels.tf | 78 ++++ modules/vpc-sc/main.tf | 338 +--------------- modules/vpc-sc/outputs.tf | 48 +-- modules/vpc-sc/service_perimeters_bridge.tf | 41 ++ modules/vpc-sc/service_perimeters_regular.tf | 311 ++++++++++++++ modules/vpc-sc/variables.tf | 191 +++++---- tests/conftest.py | 5 +- .../modules/vpc_sc/__init__.py | 18 +- tests/modules/vpc_sc/fixture/main.tf | 146 +++++++ tests/modules/vpc_sc/test_plan.py | 49 +++ tests/requirements.txt | 2 +- 12 files changed, 924 insertions(+), 681 deletions(-) create mode 100644 modules/vpc-sc/access_levels.tf create mode 100644 modules/vpc-sc/service_perimeters_bridge.tf create mode 100644 modules/vpc-sc/service_perimeters_regular.tf rename modules/vpc-sc/versions.tf => tests/modules/vpc_sc/__init__.py (63%) create mode 100644 tests/modules/vpc_sc/fixture/main.tf create mode 100644 tests/modules/vpc_sc/test_plan.py diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index fded87d1..103ce001 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -1,236 +1,148 @@ -# VPC Service Control Module +# VPC Service Controls -This module allows managing VPC Service Control (VPC-SC) properties: +This module offers a unified interface to manage VPC Service Controls [Access Policy](https://cloud.google.com/access-context-manager/docs/create-access-policy), [Access Levels](https://cloud.google.com/access-context-manager/docs/manage-access-levels), and [Service Perimeters](https://cloud.google.com/vpc-service-controls/docs/service-perimeters). -- [Access Policy](https://cloud.google.com/access-context-manager/docs/create-access-policy) -- [Access Levels](https://cloud.google.com/access-context-manager/docs/manage-access-levels) -- [VPC-SC Perimeters](https://cloud.google.com/vpc-service-controls/docs/service-perimeters) +Given the complexity of the underlying resources, the module intentionally mimics their interfaces to make it easier to map their documentation onto its variables, and reduce the internal complexity. The tradeoff is some verbosity, and a very complex type for the `service_perimeters_regular` variable (while [optional type attributes](https://www.terraform.io/language/expressions/type-constraints#experimental-optional-object-type-attributes) are still an experiment). -The Use of this module requires credentials with the [correct permissions](https://cloud.google.com/access-context-manager/docs/access-control) to use Access Context Manager. +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). -## Example VCP-SC standard perimeter +## Examples + +### Access policy + +By default, the module is configured to use an existing policy, passed in by name in the `access_policy` variable: ```hcl -module "vpc-sc" { - source = "./modules/vpc-sc" - organization_id = "organizations/112233" - access_policy_title = "My Access Policy" +module "test" { + source = "./modules/vpc-sc" + access_policy = "accessPolicies/12345678" +} +# tftest:modules=0:resources=0 +``` + +If you need the module to create the policy for you, use the `access_policy_create` variable, and set `access_policy` to `null`: + +```hcl +module "test" { + source = "./modules/vpc-sc" + access_policy = null + access_policy_create = { + parent = "organizations/123456" + title = "vpcsc-policy" + } +} +# tftest:modules=1:resources=1 +``` + +### Access levels + +As highlighted above, the `access_levels` type replicates the underlying resource structure. + +```hcl +module "test" { + source = "./modules/vpc-sc" + access_policy = "accessPolicies/12345678" access_levels = { - my_trusted_proxy = { - combining_function = "AND" + a1 = { + combining_function = null conditions = [{ - ip_subnetworks = ["85.85.85.52/32"] - required_access_levels = null - members = [] - negate = false - regions = null + members = ["user:ludomagno@google.com"], + device_policy = null, ip_subnetworks = null, negate = null, + regions = null, required_access_levels = null + }] + } + a2 = { + combining_function = "OR" + conditions = [{ + regions = ["IT", "FR"], + device_policy = null, ip_subnetworks = null, members = null, + negate = null, required_access_levels = null + },{ + ip_subnetworks = ["101.101.101.0/24"], + device_policy = null, members = null, negate = null, + regions = null, required_access_levels = null }] } } - access_level_perimeters = { - enforced = { - my_trusted_proxy = ["perimeter"] - } - } - ingress_policies = { - ingress_1 = { - ingress_from = { - identity_type = "ANY_IDENTITY" - } - ingress_to = { - resources = ["*"] - operations = { - "storage.googleapis.com" = [{ method = "google.storage.objects.create" }] - "bigquery.googleapis.com" = [{ method = "BigQueryStorage.ReadRows" }] - } - } - } - } - ingress_policies_perimeters = { - enforced = { - ingress_1 = ["default"] - } - } - - egress_policies = { - egress_1 = { - egress_from = { - identity_type = "ANY_USER_ACCOUNT" - } - egress_to = { - resources = ["*"] - operations = { - "storage.googleapis.com" = [{ method = "google.storage.objects.create" }], - "bigquery.googleapis.com" = [{ method = "BigQueryStorage.ReadRows" },{ method = "TableService.ListTables" }, { permission = "bigquery.jobs.get" }] - } - } - } - } - egress_policies_perimeters = { - enforced = { - egress_1 = ["perimeter"] - } - } - perimeters = { - perimeter = { - type = "PERIMETER_TYPE_REGULAR" - dry_run_config = null - enforced_config = { - restricted_services = ["storage.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com"] - } - } - } - perimeter_projects = { - perimeter = { - enforced = [111111111, 222222222] - } - } } -# tftest:modules=1:resources=3 -``` - -## Example VCP-SC standard perimeter with one service and one project in dry run mode -```hcl -module "vpc-sc" { - source = "./modules/vpc-sc" - organization_id = "organizations/112233" - access_policy_title = "My Access Policy" - access_levels = { - my_trusted_proxy = { - combining_function = "AND" - conditions = [{ - ip_subnetworks = ["85.85.85.52/32"] - required_access_levels = null - members = [] - negate = false - regions = null - }] - } - } - access_level_perimeters = { - enforced = { - my_trusted_proxy = ["perimeter"] - } - } - perimeters = { - perimeter = { - type = "PERIMETER_TYPE_REGULAR" - dry_run_config = { - restricted_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - } - enforced_config = { - restricted_services = ["storage.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com"] - } - } - } - perimeter_projects = { - perimeter = { - enforced = [111111111, 222222222] - dry_run = [333333333] - } - } -} -# tftest:modules=1:resources=3 -``` - -## Example VCP-SC: 2 standard perimeters with one bridge between the two (dry run mode). -```hcl -module "vpc-sc" { - source = "./modules/vpc-sc" - organization_id = "organizations/112233" - access_policy_title = "My Access Policy" - perimeters = { - perimeter_1 = { - type = "PERIMETER_TYPE_REGULAR" - dry_run_config = { - restricted_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - } - enforced_config = null - } - perimeter_2 = { - type = "PERIMETER_TYPE_REGULAR" - dry_run_config = { - restricted_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - } - enforced_config = null - } - perimeter_bridge = { - type = "PERIMETER_TYPE_BRIDGE" - dry_run_config = null - enforced_config = null - } - } - perimeter_projects = { - perimeter_1 = { - enforced = [] - dry_run = [111111111] - } - perimeter_2 = { - enforced = [] - dry_run = [222222222] - } - perimeter_bridge = { - enforced = [] - dry_run = [111111111, 222222222] - } - } -} -# tftest:modules=1:resources=4 -``` - -## Example VCP-SC standard perimeter with one service and one project in dry run mode in a Organization with an already existent access policy -```hcl -module "vpc-sc-first" { - source = "./modules/vpc-sc" - organization_id = "organizations/112233" - access_policy_create = false - access_policy_name = "My Access Policy" - access_levels = { - my_trusted_proxy = { - combining_function = "AND" - conditions = [{ - ip_subnetworks = ["85.85.85.52/32"] - required_access_levels = null - members = [] - negate = false - regions = null - }] - } - } - access_level_perimeters = { - enforced = { - my_trusted_proxy = ["perimeter"] - } - } - perimeters = { - perimeter = { - type = "PERIMETER_TYPE_REGULAR" - dry_run_config = { - restricted_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com", "bigquery.googleapis.com"] - } - enforced_config = { - restricted_services = ["storage.googleapis.com"] - vpc_accessible_services = ["storage.googleapis.com"] - } - } - } - perimeter_projects = { - perimeter = { - enforced = [111111111, 222222222] - dry_run = [333333333] - } - } -} - # tftest:modules=1:resources=2 ``` +### Service perimeters + +Bridge and regulare 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, simply use the module's outputs in the provided variables. The example below shows how to do this in practice. + +Resources for both perimeters have a `lifecycle` block that ignores changes to `spec` and `status` resources (projects), to allow using the additive resource `google_access_context_manager_service_perimeter_resource` at project creation. If this is not needed, the `lifecycle` blocks can be safely commented in the code. + +#### Bridge type + +```hcl +module "test" { + source = "./modules/vpc-sc" + access_policy = "accessPolicies/12345678" + service_perimeters_bridge = { + b1 = { + status_resources = ["projects/111110", "projects/111111"] + spec_resources = null + use_explicit_dry_run_spec = false + } + b2 = { + status_resources = ["projects/222220", "projects/222221"] + spec_resources = ["projects/222220", "projects/222221"] + use_explicit_dry_run_spec = true + } + } +} +# tftest:modules=1:resources=2 +``` + +#### Regular type + +```hcl +module "test" { + source = "./modules/vpc-sc" + access_policy = "accessPolicies/12345678" + access_levels = { + a1 = { + combining_function = null + conditions = [{ + members = ["user:ludomagno@google.com"], + device_policy = null, ip_subnetworks = null, negate = null, + regions = null, required_access_levels = null + }] + } + } + service_perimeters_regular = { + r1 = { + spec = null + status = { + access_levels = [module.test.access_level_names["a1"]] + resources = ["projects/11111", "projects/111111"] + restricted_services = ["storage.googleapis.com"] + egress_policies = null + ingress_policies = null + vpc_accessible_services = { + allowed_services = ["compute.googleapis.com"] + enable_restriction = true + } + } + use_explicit_dry_run_spec = false + } + } +} +# tftest:modules=1:resources=2 +``` + +## TODO + +- [ ] implement support for the `google_access_context_manager_gcp_user_access_binding` resource + + + @@ -238,28 +150,24 @@ module "vpc-sc-first" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| organization_id | Organization id in organizations/nnnnnn format. | string | ✓ | | -| access_level_perimeters | Enforced mode -> Access Level -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run' | map(map(list(string))) | | {} | -| access_levels | Map of Access Levels to be created. For each Access Level you can specify 'ip_subnetworks, required_access_levels, members, negate or regions'. | map(object({…})) | | {} | -| access_policy_create | Enable autocreation of the Access Policy | bool | | true | -| access_policy_name | Referenced Access Policy name | string | | null | -| access_policy_title | Access Policy title to be created. | string | | null | -| egress_policies | List of EgressPolicies in the form described in the [documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#egress_policies) | | | null | -| egress_policies_perimeters | Enforced mode -> Egress Policy -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run' | map(map(list(string))) | | {} | -| ingress_policies | List of IngressPolicies in the form described in the [documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#ingress_policies) | | | null | -| ingress_policies_perimeters | Enforced mode -> Ingress Policy -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run' | map(map(list(string))) | | {} | -| perimeter_projects | Perimeter -> Enforced Mode -> Projects Number mapping. Enforced mode can be 'enforced' or 'dry_run'. | map(map(list(number))) | | {} | -| perimeters | Set of Perimeters. | map(object({…})) | | {} | +| access_policy | Access Policy name, leave null to use auto-created one. | string | ✓ | | +| access_levels | Map of access levels in name => [conditions] format. | map(object({…})) | | {} | +| access_policy_create | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format. | object({…}) | | null | +| service_perimeters_bridge | Bridge service perimeters. | map(object({…})) | | {} | +| service_perimeters_regular | Regular service perimeters. | map(object({…})) | | {} | ## Outputs | name | description | sensitive | |---|---|:---:| -| access_levels | Access Levels. | | -| access_policy_name | Access Policy resource | | -| organization_id | Organization id dependent on module resources. | | -| perimeters_bridge | VPC-SC bridge perimeter resources. | | -| perimeters_standard | VPC-SC standard perimeter resources. | | +| access_level_names | Access level resources. | | +| access_levels | Access level resources. | | +| access_policy | Access policy resource, if autocreated. | | +| access_policy_name | Access policy name. | | +| service_perimeters_bridge | Bridge service perimeter resources. | | +| service_perimeters_regular | Regular service perimeter resources. | | + + diff --git a/modules/vpc-sc/access_levels.tf b/modules/vpc-sc/access_levels.tf new file mode 100644 index 00000000..585570ee --- /dev/null +++ b/modules/vpc-sc/access_levels.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2021 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. + */ + +# TODO(ludomagno): add a second variable and resource for custom access levels + +# this code implements "additive" access levels, if "authoritative" +# access levels are needed, switch to the +# google_access_context_manager_access_levels resource + +resource "google_access_context_manager_access_level" "basic" { + for_each = var.access_levels + parent = "accessPolicies/${local.access_policy}" + name = "accessPolicies/${local.access_policy}/accessLevels/${each.key}" + title = each.key + basic { + combining_function = each.value.combining_function + dynamic "conditions" { + for_each = toset( + each.value.conditions == null ? [] : each.value.conditions + ) + iterator = condition + content { + dynamic "device_policy" { + for_each = toset( + condition.key.device_policy == null ? [] : [condition.key.device_policy] + ) + iterator = device_policy + content { + dynamic "os_constraints" { + for_each = toset( + device_policy.key.os_constraints == null ? [] : device_policy.key.os_constraints + ) + iterator = os_constraint + content { + minimum_version = os_constraint.key.minimum_version + os_type = os_constraint.key.os_type + require_verified_chrome_os = os_constraint.key.require_verified_chrome_os + } + } + allowed_encryption_statuses = device_policy.key.allowed_encryption_statuses + allowed_device_management_levels = device_policy.key.allowed_device_management_levels + require_admin_approval = device_policy.key.require_admin_approval + require_corp_owned = device_policy.key.require_corp_owned + require_screen_lock = device_policy.key.require_screen_lock + } + } + ip_subnetworks = ( + condition.key.ip_subnetworks == null ? [] : condition.key.ip_subnetworks + ) + members = ( + condition.key.members == null ? [] : condition.key.members + ) + negate = condition.key.negate + regions = ( + condition.key.regions == null ? [] : condition.key.regions + ) + required_access_levels = ( + condition.key.required_access_levels == null + ? [] + : condition.key.required_access_levels + ) + } + } + } +} diff --git a/modules/vpc-sc/main.tf b/modules/vpc-sc/main.tf index 3e6c2801..73947036 100644 --- a/modules/vpc-sc/main.tf +++ b/modules/vpc-sc/main.tf @@ -15,340 +15,14 @@ */ locals { - access_policy_name = ( - var.access_policy_create - ? try(google_access_context_manager_access_policy.default[0].name, null) - : var.access_policy_name + access_policy = try( + google_access_context_manager_access_policy.default.0.name, + var.access_policy ) - - standard_perimeters = { - for key, value in var.perimeters : - key => value if value.type == "PERIMETER_TYPE_REGULAR" - } - - bridge_perimeters = { - for key, value in var.perimeters : - key => value if value.type == "PERIMETER_TYPE_BRIDGE" - } - - perimeter_access_levels_enforced = try(transpose(var.access_level_perimeters.enforced), null) - perimeter_access_levels_dry_run = try(transpose(var.access_level_perimeters.dry_run), null) - perimeter_ingress_policies_enforced = try(transpose(var.ingress_policies_perimeters.enforced), null) - perimeter_ingress_policies_dry_run = try(transpose(var.ingress_policies_perimeters.dry_run), null) - perimeter_egress_policies_enforced = try(transpose(var.egress_policies_perimeters.enforced), null) - perimeter_egress_policies_dry_run = try(transpose(var.egress_policies_perimeters.dry_run), null) } resource "google_access_context_manager_access_policy" "default" { - count = var.access_policy_create ? 1 : 0 - parent = var.organization_id - title = var.access_policy_title == null ? "${var.organization_id}-title" : var.access_policy_title -} - -resource "google_access_context_manager_access_level" "default" { - for_each = var.access_levels - parent = "accessPolicies/${local.access_policy_name}" - name = "accessPolicies/${local.access_policy_name}/accessLevels/${each.key}" - title = each.key - - dynamic "basic" { - for_each = try(toset(each.value.conditions), []) - iterator = condition - - content { - combining_function = try(each.value.combining_function, null) - conditions { - ip_subnetworks = try(condition.value.ip_subnetworks, null) - required_access_levels = try(condition.value.required_access_levels, null) - members = try(condition.value.members, null) - negate = try(condition.value.negate, null) - regions = try(condition.value.regions, null) - } - } - } -} - -resource "google_access_context_manager_service_perimeter" "standard" { - for_each = local.standard_perimeters - parent = "accessPolicies/${local.access_policy_name}" - description = "Terraform managed." - name = "accessPolicies/${local.access_policy_name}/servicePerimeters/${each.key}" - title = each.key - perimeter_type = each.value.type - - # Enforced mode configuration - dynamic "status" { - for_each = each.value.enforced_config != null ? [""] : [] - - content { - resources = formatlist( - "projects/%s", try(lookup(var.perimeter_projects, each.key, {}).enforced, []) - ) - restricted_services = each.value.enforced_config.restricted_services - access_levels = formatlist( - "accessPolicies/${local.access_policy_name}/accessLevels/%s", - try(lookup(local.perimeter_access_levels_enforced, each.key, []), []) - ) - - dynamic "vpc_accessible_services" { - for_each = try(length(each.value.enforced_config.vpc_accessible_services) != 0 ? [""] : [], []) - - content { - enable_restriction = true - allowed_services = each.value.enforced_config.vpc_accessible_services - } - } - - dynamic "egress_policies" { - for_each = try(local.perimeter_egress_policies_enforced[each.key] != null ? local.perimeter_egress_policies_enforced[each.key] : [], []) - - content { - dynamic "egress_from" { - for_each = try(var.egress_policies[egress_policies.value].egress_from != null ? [""] : [], []) - - content { - identity_type = try(var.egress_policies[egress_policies.value].egress_from.identity_type, null) - identities = try(var.egress_policies[egress_policies.value].egress_from.identities, null) - } - } - dynamic "egress_to" { - for_each = try(var.egress_policies[egress_policies.value].egress_to != null ? [""] : [], []) - - content { - resources = try(var.egress_policies[egress_policies.value].egress_to.resources, null) - - dynamic "operations" { - for_each = try(var.egress_policies[egress_policies.value].egress_to.operations, []) - - content { - service_name = try(operations.key, null) - - dynamic "method_selectors" { - for_each = try(operations.value, []) - - content { - method = try(method_selectors.value.method, null) - permission = try(method_selectors.value.permission, null) - } - } - } - } - } - } - } - } - - dynamic "ingress_policies" { - for_each = try(local.perimeter_ingress_policies_enforced[each.key] != null ? local.perimeter_ingress_policies_enforced[each.key] : [], []) - - content { - dynamic "ingress_from" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_from != null ? [""] : [], []) - - content { - identity_type = try(var.ingress_policies[ingress_policies.value].ingress_from.identity_type, null) - identities = try(var.ingress_policies[ingress_policies.value].ingress_from.identities, null) - - dynamic "sources" { - for_each = toset(try([var.ingress_policies[ingress_policies.value].ingress_from.sources], [])) - - content { - access_level = try(sources.value.access_level, null) - resource = try(sources.value.resource, null) - } - } - } - } - dynamic "ingress_to" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_to != null ? [""] : [], []) - - content { - resources = try(var.ingress_policies[ingress_policies.value].ingress_to.resources, null) - - dynamic "operations" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_to.operations, []) - - content { - service_name = try(operations.key, null) - - dynamic "method_selectors" { - for_each = try(operations.value, []) - - content { - method = try(method_selectors.value.method, null) - permission = try(method_selectors.value.permission, null) - } - } - } - } - } - } - } - } - } - } - - # Dry run mode configuration - use_explicit_dry_run_spec = each.value.dry_run_config != null ? true : false - dynamic "spec" { - for_each = each.value.dry_run_config != null ? [""] : [] - - content { - resources = formatlist( - "projects/%s", try(lookup(var.perimeter_projects, each.key, {}).dry_run, []) - ) - restricted_services = try(each.value.dry_run_config.restricted_services, null) - access_levels = formatlist( - "accessPolicies/${local.access_policy_name}/accessLevels/%s", - try(lookup(local.perimeter_access_levels_dry_run, each.key, []), []) - ) - - dynamic "vpc_accessible_services" { - for_each = try(length(each.value.dry_run_config.vpc_accessible_services) != 0 ? [""] : [], []) - - content { - enable_restriction = true - allowed_services = try(each.value.dry_run_config.vpc_accessible_services, null) - } - } - - dynamic "egress_policies" { - for_each = try(local.perimeter_egress_policies_dry_run[each.key] != null ? local.perimeter_egress_policies_dry_run[each.key] : [], []) - - content { - dynamic "egress_from" { - for_each = try(var.egress_policies[egress_policies.value].egress_from != null ? [""] : [], []) - - content { - identity_type = try(var.egress_policies[egress_policies.value].egress_from.identity_type, null) - identities = try(var.egress_policies[egress_policies.value].egress_from.identities, null) - } - } - dynamic "egress_to" { - for_each = try(var.egress_policies[egress_policies.value].egress_to != null ? [""] : [], []) - - content { - resources = try(var.egress_policies[egress_policies.value].egress_to.resources, null) - - dynamic "operations" { - for_each = try(var.egress_policies[egress_policies.value].egress_to.operations, []) - - content { - service_name = try(operations.key, null) - - dynamic "method_selectors" { - for_each = try(operations.value, []) - - content { - method = try(method_selectors.value.method, null) - permission = try(method_selectors.value.permission, null) - } - } - } - } - } - } - } - } - - dynamic "ingress_policies" { - for_each = try(local.perimeter_ingress_policies_dry_run[each.key] != null ? local.perimeter_ingress_policies_dry_run[each.key] : [], []) - - content { - dynamic "ingress_from" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_from != null ? [""] : [], []) - - content { - identity_type = try(var.ingress_policies[ingress_policies.value].ingress_from.identity_type, null) - identities = try(var.ingress_policies[ingress_policies.value].ingress_from.identities, null) - - dynamic "sources" { - for_each = toset(try([var.ingress_policies[ingress_policies.value].ingress_from.sources], [])) - - content { - access_level = try(sources.value.access_level, null) - resource = try(sources.value.resource, null) - } - } - } - } - dynamic "ingress_to" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_to != null ? [""] : [], []) - - content { - resources = try(var.ingress_policies[ingress_policies.value].ingress_to.resources, null) - - dynamic "operations" { - for_each = try(var.ingress_policies[ingress_policies.value].ingress_to.operations, []) - - content { - service_name = try(operations.key, null) - - dynamic "method_selectors" { - for_each = try(operations.value, []) - - content { - method = try(method_selectors.value.method, null) - permission = try(method_selectors.value.permission, null) - } - } - } - } - } - } - } - } - } - } - - # Uncomment if used alongside `google_access_context_manager_service_perimeter_resource`, - # so they don't fight over which resources should be in the policy. - # lifecycle { - # ignore_changes = [status[0].resources] - # } - - depends_on = [ - google_access_context_manager_access_level.default, - ] -} - -resource "google_access_context_manager_service_perimeter" "bridge" { - for_each = local.bridge_perimeters - parent = "accessPolicies/${local.access_policy_name}" - description = "Terraform managed." - name = "accessPolicies/${local.access_policy_name}/servicePerimeters/${each.key}" - title = each.key - perimeter_type = each.value.type - - # Enforced mode configuration - dynamic "status" { - for_each = try(lookup(var.perimeter_projects, each.key, {}).enforced, []) != null ? [""] : [] - - content { - resources = formatlist("projects/%s", try(lookup(var.perimeter_projects, each.key, {}).enforced, [])) - } - } - - # Dry run mode configuration - use_explicit_dry_run_spec = try(lookup(var.perimeter_projects, each.key, null).dry_run, null) != null ? true : null - dynamic "spec" { - for_each = try(lookup(var.perimeter_projects, each.key, null).dry_run, null) != null ? [""] : [] - - content { - resources = try(formatlist("projects/%s", lookup(var.perimeter_projects, each.key, {}).dry_run), null) - restricted_services = [] - access_levels = [] - } - } - - # Uncomment if used alongside `google_access_context_manager_service_perimeter_resource`, - # so they don't fight over which resources should be in the policy. - # lifecycle { - # ignore_changes = [status[0].resources] - # } - - depends_on = [ - google_access_context_manager_service_perimeter.standard, - google_access_context_manager_access_level.default, - ] + count = var.access_policy_create != null ? 1 : 0 + parent = var.access_policy_create.parent + title = var.access_policy_create.title } diff --git a/modules/vpc-sc/outputs.tf b/modules/vpc-sc/outputs.tf index 9a068b92..3412e234 100644 --- a/modules/vpc-sc/outputs.tf +++ b/modules/vpc-sc/outputs.tf @@ -14,39 +14,35 @@ * limitations under the License. */ -output "access_levels" { - description = "Access Levels." +output "access_level_names" { + description = "Access level resources." value = { - for key, value in google_access_context_manager_access_level.default : - key => value + for k, v in google_access_context_manager_access_level.basic : + k => v.name } } +output "access_levels" { + description = "Access level resources." + value = google_access_context_manager_access_level.basic +} + +output "access_policy" { + description = "Access policy resource, if autocreated." + value = try(google_access_context_manager_access_policy.default.0, null) +} + output "access_policy_name" { - description = "Access Policy resource" - value = local.access_policy_name + description = "Access policy name." + value = local.access_policy } -output "organization_id" { - description = "Organization id dependent on module resources." - value = var.organization_id - depends_on = [ - google_access_context_manager_access_policy.default - ] +output "service_perimeters_bridge" { + description = "Bridge service perimeter resources." + value = google_access_context_manager_service_perimeter.bridge } -output "perimeters_bridge" { - description = "VPC-SC bridge perimeter resources." - value = { - for key, value in google_access_context_manager_service_perimeter.bridge : - key => value - } -} - -output "perimeters_standard" { - description = "VPC-SC standard perimeter resources." - value = { - for key, value in google_access_context_manager_service_perimeter.standard : - key => value - } +output "service_perimeters_regular" { + description = "Regular service perimeter resources." + value = google_access_context_manager_service_perimeter.regular } diff --git a/modules/vpc-sc/service_perimeters_bridge.tf b/modules/vpc-sc/service_perimeters_bridge.tf new file mode 100644 index 00000000..c1d5130f --- /dev/null +++ b/modules/vpc-sc/service_perimeters_bridge.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2021 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. + */ + +# this code implements "additive" service perimeters, if "authoritative" +# service perimeters are needed, switch to the +# google_access_context_manager_service_perimeters resource + +resource "google_access_context_manager_service_perimeter" "bridge" { + for_each = var.service_perimeters_bridge + 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 + spec { + resources = each.value.spec_resources + } + status { + resources = each.value.status_resources + } + 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 + ] +} diff --git a/modules/vpc-sc/service_perimeters_regular.tf b/modules/vpc-sc/service_perimeters_regular.tf new file mode 100644 index 00000000..2f4eae3c --- /dev/null +++ b/modules/vpc-sc/service_perimeters_regular.tf @@ -0,0 +1,311 @@ +/** + * Copyright 2021 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. + */ + +# this code implements "additive" service perimeters, if "authoritative" +# service perimeters are needed, switch to the +# google_access_context_manager_service_perimeters resource + +resource "google_access_context_manager_service_perimeter" "regular" { + for_each = var.service_perimeters_regular + parent = "accessPolicies/${local.access_policy}" + name = "accessPolicies/${local.access_policy}/servicePerimeters/${each.key}" + title = each.key + perimeter_type = "PERIMETER_TYPE_REGULAR" + use_explicit_dry_run_spec = each.value.use_explicit_dry_run_spec + dynamic "spec" { + for_each = each.value.spec == null ? {} : { 1 = 1 } + content { + access_levels = each.value.spec.access_levels + resources = each.value.spec.resources + restricted_services = each.value.spec.restricted_services + # begin egress_policies + dynamic "egress_policies" { + for_each = toset( + each.value.spec.egress_policies == null + ? [] + : each.value.spec.egress_policies + ) + iterator = policy + content { + # begin egress_from + dynamic "egress_from" { + for_each = policy.key.egress_from == null ? {} : { 1 = 1 } + content { + identity_type = policy.key.egress_from.identity_type + identities = policy.key.egress_from.identities + } + } + # end egress_from + # begin egress_to + dynamic "egress_to" { + for_each = policy.key.egress_to == null ? {} : { 1 = 1 } + content { + resources = policy.key.egress_to.resources + dynamic "operations" { + for_each = toset( + policy.key.egress_to.operations == null + ? [] + : policy.key.egress_to.operations + ) + iterator = operation + content { + service_name = operation.service_name + dynamic "method_selectors" { + for_each = toset( + operation.key.method_selectors == null + ? [] + : operation.key.method_selectors + ) + content { + method = method_selectors.key + } + } + } + } + } + } + # end egress_to + } + } + # end egress_policies + # begin ingress_policies + dynamic "ingress_policies" { + for_each = toset( + each.value.spec.ingress_policies == null + ? [] + : each.value.spec.ingress_policies + ) + iterator = policy + content { + # begin ingress_from + dynamic "ingress_from" { + for_each = policy.key.ingress_from == null ? {} : { 1 = 1 } + content { + identity_type = policy.key.ingress_from.identity_type + identities = policy.key.ingress_from.identities + # begin sources + dynamic "sources" { + for_each = toset( + policy.key.ingress_from.source_access_levels == null + ? [] + : policy.key.ingress_from.source_access_levels + ) + content { + access_level = sources.key + } + } + dynamic "sources" { + for_each = toset( + policy.key.ingress_from.source_resources == null + ? [] + : policy.key.ingress_from.source_resources + ) + content { + resource = sources.key + } + } + # end sources + } + } + # end ingress_from + # begin ingress_to + dynamic "ingress_to" { + for_each = policy.key.ingress_to == null ? {} : { 1 = 1 } + content { + resources = policy.key.ingress_to.resources + dynamic "operations" { + for_each = toset( + policy.key.ingress_to.operations == null + ? [] + : policy.key.ingress_to.operations + ) + iterator = operation + content { + service_name = operation.service_name + dynamic "method_selectors" { + for_each = toset( + operation.key.method_selectors == null + ? [] + : operation.key.method_selectors + ) + content { + method = method_selectors.key + } + } + } + } + } + } + # end ingress_to + } + } + # end ingress_policies + # begin vpc_accessible_services + dynamic "vpc_accessible_services" { + for_each = each.value.spec.vpc_accessible_services == null ? {} : { 1 = 1 } + content { + allowed_services = each.value.spec.vpc_accessible_services.allowed_services + enable_restriction = each.value.spec.vpc_accessible_services.enable_restriction + } + } + # end vpc_accessible_services + } + } + dynamic "status" { + for_each = each.value.status == null ? {} : { 1 = 1 } + content { + access_levels = each.value.status.access_levels + resources = each.value.status.resources + restricted_services = each.value.status.restricted_services + # begin egress_policies + dynamic "egress_policies" { + for_each = toset( + each.value.status.egress_policies == null + ? [] + : each.value.status.egress_policies + ) + iterator = policy + content { + # begin egress_from + dynamic "egress_from" { + for_each = policy.key.egress_from == null ? {} : { 1 = 1 } + content { + identity_type = policy.key.egress_from.identity_type + identities = policy.key.egress_from.identities + } + } + # end egress_from + # begin egress_to + dynamic "egress_to" { + for_each = policy.key.egress_to == null ? {} : { 1 = 1 } + content { + resources = policy.key.egress_to.resources + dynamic "operations" { + for_each = toset( + policy.key.egress_to.operations == null + ? [] + : policy.key.egress_to.operations + ) + content { + service_name = operations.key.service_name + dynamic "method_selectors" { + for_each = toset( + operations.key.method_selectors == null + ? [] + : operations.key.method_selectors + ) + content { + method = method_selectors.key + } + } + } + } + } + } + # end egress_to + } + } + # end egress_policies + # begin ingress_policies + dynamic "ingress_policies" { + for_each = toset( + each.value.status.ingress_policies == null + ? [] + : each.value.status.ingress_policies + ) + iterator = policy + content { + # begin ingress_from + dynamic "ingress_from" { + for_each = policy.key.ingress_from == null ? {} : { 1 = 1 } + content { + identity_type = policy.key.ingress_from.identity_type + identities = policy.key.ingress_from.identities + # begin sources + dynamic "sources" { + for_each = toset( + policy.key.ingress_from.source_access_levels == null + ? [] + : policy.key.ingress_from.source_access_levels + ) + content { + access_level = sources.key + } + } + dynamic "sources" { + for_each = toset( + policy.key.ingress_from.source_resources == null + ? [] + : policy.key.ingress_from.source_resources + ) + content { + resource = sources.key + } + } + # end sources + } + } + # end ingress_from + # begin ingress_to + dynamic "ingress_to" { + for_each = policy.key.ingress_to == null ? {} : { 1 = 1 } + content { + resources = policy.key.ingress_to.resources + dynamic "operations" { + for_each = toset( + policy.key.ingress_to.operations == null + ? [] + : policy.key.ingress_to.operations + ) + content { + service_name = operations.key.service_name + dynamic "method_selectors" { + for_each = toset( + operations.key.method_selectors == null + ? [] + : operations.key.method_selectors + ) + content { + method = method_selectors.key + } + } + } + } + } + } + # end ingress_to + } + } + # end ingress_policies + # begin vpc_accessible_services + dynamic "vpc_accessible_services" { + for_each = each.value.status.vpc_accessible_services == null ? {} : { 1 = 1 } + content { + allowed_services = each.value.status.vpc_accessible_services.allowed_services + enable_restriction = each.value.status.vpc_accessible_services.enable_restriction + } + } + # end vpc_accessible_services + } + } + 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 + ] +} diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index 866f9ef2..fcfef359 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.tf @@ -14,90 +14,145 @@ * limitations under the License. */ -variable "access_level_perimeters" { - description = "Enforced mode -> Access Level -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run'" - type = map(map(list(string))) - default = {} -} - variable "access_levels" { - description = "Map of Access Levels to be created. For each Access Level you can specify 'ip_subnetworks, required_access_levels, members, negate or regions'." + description = "Map of access levels in name => [conditions] format." type = map(object({ combining_function = string conditions = list(object({ + device_policy = object({ + require_screen_lock = bool + allowed_encryption_statuses = list(string) + allowed_device_management_levels = list(string) + os_constraints = list(object({ + minimum_version = string + os_type = string + require_verified_chrome_os = bool + })) + require_admin_approval = bool + require_corp_owned = bool + }) ip_subnetworks = list(string) - required_access_levels = list(string) members = list(string) - negate = string + negate = bool regions = list(string) + required_access_levels = list(string) })) })) default = {} + validation { + condition = alltrue([ + for k, v in var.access_levels : ( + v.combining_function == null || + v.combining_function == "AND" || + v.combining_function == "OR" + ) + ]) + error_message = "Invalid `combining_function` value (null, \"AND\", \"OR\" accepted)." + } +} + +variable "access_policy" { + description = "Access Policy name, leave null to use auto-created one." + type = string } variable "access_policy_create" { - description = "Enable autocreation of the Access Policy" - type = bool - default = true + description = "Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format." + type = object({ + parent = string + title = string + }) + default = null } -variable "access_policy_name" { - description = "Referenced Access Policy name" - type = string - default = null -} - -variable "access_policy_title" { - description = "Access Policy title to be created." - type = string - default = null -} - -variable "egress_policies" { - description = "List of EgressPolicies in the form described in the [documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#egress_policies)" - default = null -} - -variable "egress_policies_perimeters" { - description = "Enforced mode -> Egress Policy -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run'" - type = map(map(list(string))) - default = {} -} - -variable "ingress_policies" { - description = "List of IngressPolicies in the form described in the [documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#ingress_policies)" - default = null -} - -variable "ingress_policies_perimeters" { - description = "Enforced mode -> Ingress Policy -> Perimeters mapping. Enforced mode can be 'enforced' or 'dry_run'" - type = map(map(list(string))) - default = {} -} - -variable "organization_id" { - description = "Organization id in organizations/nnnnnn format." - type = string -} - -variable "perimeter_projects" { - description = "Perimeter -> Enforced Mode -> Projects Number mapping. Enforced mode can be 'enforced' or 'dry_run'." - type = map(map(list(number))) - default = {} -} - -variable "perimeters" { - description = "Set of Perimeters." +variable "service_perimeters_bridge" { + description = "Bridge service perimeters." type = map(object({ - type = string - dry_run_config = object({ - restricted_services = list(string) - vpc_accessible_services = list(string) - }) - enforced_config = object({ - restricted_services = list(string) - vpc_accessible_services = list(string) - }) + spec_resources = list(string) + status_resources = list(string) + use_explicit_dry_run_spec = bool + })) + default = {} +} + +variable "service_perimeters_regular" { + description = "Regular service perimeters." + type = map(object({ + spec = object({ + access_levels = list(string) + resources = list(string) + restricted_services = list(string) + egress_policies = list(object({ + egress_from = object({ + identity_type = string + identities = list(string) + }) + egress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + ingress_policies = list(object({ + ingress_from = object({ + identity_type = string + identities = list(string) + source_access_levels = list(string) + source_resources = list(string) + }) + ingress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + vpc_accessible_services = object({ + allowed_services = list(string) + enable_restriction = bool + }) + }) + status = object({ + access_levels = list(string) + resources = list(string) + restricted_services = list(string) + egress_policies = list(object({ + egress_from = object({ + identity_type = string + identities = list(string) + }) + egress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + ingress_policies = list(object({ + ingress_from = object({ + identity_type = string + identities = list(string) + source_access_levels = list(string) + source_resources = list(string) + }) + ingress_to = object({ + operations = list(object({ + method_selectors = list(string) + service_name = string + })) + resources = list(string) + }) + })) + vpc_accessible_services = object({ + allowed_services = list(string) + enable_restriction = bool + }) + }) + use_explicit_dry_run_spec = bool })) default = {} } diff --git a/tests/conftest.py b/tests/conftest.py index 33c63596..ad8cc5ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,9 +79,10 @@ def example_plan_runner(_plan_runner): "Runs Terraform plan and returns count of modules and resources." plan = _plan_runner(fixture_path) # the fixture is the example we are testing + modules = plan.modules or {} return ( - len(plan.modules), - sum(len(m.resources) for m in plan.modules.values())) + len(modules), + sum(len(m.resources) for m in modules.values())) return run_plan diff --git a/modules/vpc-sc/versions.tf b/tests/modules/vpc_sc/__init__.py similarity index 63% rename from modules/vpc-sc/versions.tf rename to tests/modules/vpc_sc/__init__.py index 1cc6bf89..d46dbae5 100644 --- a/modules/vpc-sc/versions.tf +++ b/tests/modules/vpc_sc/__init__.py @@ -4,26 +4,10 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# 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. - -terraform { - required_version = ">= 1.0.0" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 4.0.0" - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 4.0.0" - } - } -} - - diff --git a/tests/modules/vpc_sc/fixture/main.tf b/tests/modules/vpc_sc/fixture/main.tf new file mode 100644 index 00000000..f13f5f82 --- /dev/null +++ b/tests/modules/vpc_sc/fixture/main.tf @@ -0,0 +1,146 @@ +/** + * Copyright 2021 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. + */ + +variable "access_policy" { + description = "Access Policy name, leave null to use auto-created one." + type = string + default = null +} + +variable "access_policy_create" { + description = "Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format." + type = object({ + parent = string + title = string + }) + default = { + parent = "organizations/123456" + title = "vpcsc-policy" + } +} + +module "test" { + source = "../../../../modules/vpc-sc" + access_policy = var.access_policy + access_policy_create = var.access_policy_create + access_levels = { + a1 = { + combining_function = null + conditions = [ + { + device_policy = null + ip_subnetworks = null + members = ["user:ludomagno@google.com"] + negate = null + regions = null + required_access_levels = null + } + ] + } + a2 = { + combining_function = "OR" + conditions = [ + { + device_policy = null + ip_subnetworks = null + members = null + negate = null + regions = ["IT", "FR"] + required_access_levels = null + }, + { + device_policy = null + ip_subnetworks = null + members = null + negate = null + regions = ["US"] + required_access_levels = null + } + ] + } + } + service_perimeters_bridge = { + b1 = { + status_resources = ["projects/111110", "projects/111111"] + spec_resources = null + use_explicit_dry_run_spec = false + } + b2 = { + status_resources = ["projects/111110", "projects/222220"] + spec_resources = ["projects/111110", "projects/222220"] + use_explicit_dry_run_spec = true + } + } + service_perimeters_regular = { + r1 = { + spec = null + status = { + access_levels = [module.test.access_level_names["a1"]] + resources = ["projects/11111", "projects/111111"] + restricted_services = ["storage.googleapis.com"] + egress_policies = null + ingress_policies = null + vpc_accessible_services = { + allowed_services = ["compute.googleapis.com"] + enable_restriction = true + } + } + use_explicit_dry_run_spec = false + } + r2 = { + spec = null + status = { + access_levels = [module.test.access_level_names["a1"]] + resources = ["projects/222220", "projects/222221"] + restricted_services = ["storage.googleapis.com"] + egress_policies = [ + { + egress_from = { + identity_type = null + identities = ["user:foo@example.com"] + } + egress_to = { + operations = null + resources = ["projects/333330"] + } + } + ] + ingress_policies = [ + { + ingress_from = { + identity_type = null + identities = null + source_access_levels = [module.test.access_level_names["a2"]] + source_resources = ["projects/333330"] + } + ingress_to = { + operations = [{ + method_selectors = null + service_name = "compute.googleapis.com" + }] + resources = ["projects/222220"] + } + } + ] + vpc_accessible_services = { + allowed_services = ["compute.googleapis.com"] + enable_restriction = true + } + } + use_explicit_dry_run_spec = false + } + } +} diff --git a/tests/modules/vpc_sc/test_plan.py b/tests/modules/vpc_sc/test_plan.py new file mode 100644 index 00000000..b4b88547 --- /dev/null +++ b/tests/modules/vpc_sc/test_plan.py @@ -0,0 +1,49 @@ +# Copyright 2021 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. + + +import os + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + + +def test_create_policy(plan_runner): + "Test with auto-created policy." + _, resources = plan_runner(FIXTURES_DIR) + counts = {} + for r in resources: + n = f'{r["type"]}.{r["name"]}' + counts[n] = counts.get(n, 0) + 1 + assert counts == { + 'google_access_context_manager_access_level.basic': 2, + 'google_access_context_manager_access_policy.default': 1, + 'google_access_context_manager_service_perimeter.bridge': 2, + 'google_access_context_manager_service_perimeter.regular': 2 + } + + +def test_use_policy(plan_runner): + "Test with existing policy." + _, resources = plan_runner(FIXTURES_DIR, access_policy_create="null", + access_policy="accessPolicies/foobar") + counts = {} + for r in resources: + n = f'{r["type"]}.{r["name"]}' + counts[n] = counts.get(n, 0) + 1 + assert counts == { + 'google_access_context_manager_access_level.basic': 2, + 'google_access_context_manager_service_perimeter.bridge': 2, + 'google_access_context_manager_service_perimeter.regular': 2 + } diff --git a/tests/requirements.txt b/tests/requirements.txt index 480dbeb5..b7c85499 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,4 @@ pytest>=4.6.0 PyYAML>=5.3 -tftest>=1.6.1 +tftest>=1.6.2 marko>=0.9.1