VPC-SC module factories (#2081)
* factory untested * factory example test
This commit is contained in:
parent
b408494a74
commit
91615e0140
|
@ -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).
|
||||
|
||||
<!-- BEGIN TOC -->
|
||||
- [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)
|
||||
<!-- END TOC -->
|
||||
|
||||
## 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. | <code>google_access_context_manager_access_level</code> |
|
||||
| [factory.tf](./factory.tf) | None | |
|
||||
| [iam.tf](./iam.tf) | IAM bindings | <code>google_access_context_manager_access_policy_iam_binding</code> · <code>google_access_context_manager_access_policy_iam_member</code> |
|
||||
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_access_context_manager_access_policy</code> |
|
||||
| [outputs.tf](./outputs.tf) | Module outputs. | |
|
||||
|
@ -225,12 +320,13 @@ module "test" {
|
|||
| [access_levels](variables.tf#L17) | Access level definitions. | <code title="map(object({ combining_function = optional(string) conditions = optional(list(object({ device_policy = optional(object({ allowed_device_management_levels = optional(list(string)) allowed_encryption_statuses = optional(list(string)) require_admin_approval = bool require_corp_owned = bool require_screen_lock = optional(bool) os_constraints = optional(list(object({ os_type = string minimum_version = optional(string) require_verified_chrome_os = optional(bool) }))) })) ip_subnetworks = optional(list(string), []) members = optional(list(string), []) negate = optional(bool) regions = optional(list(string), []) required_access_levels = optional(list(string), []) })), []) description = optional(string) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [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. | <code title="object({ parent = string title = string scopes = optional(list(string), null) })">object({…})</code> | | <code>null</code> |
|
||||
| [egress_policies](variables.tf#L71) | Egress policy definitions that can be referenced in perimeters. | <code title="map(object({ from = object({ identity_type = optional(string) identities = optional(list(string)) }) to = object({ 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) }) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L102) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L108) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L123) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [ingress_policies](variables.tf#L138) | Ingress policy definitions that can be referenced in perimeters. | <code title="map(object({ from = object({ access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) resources = optional(list(string), []) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) permission_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) }) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [service_perimeters_bridge](variables.tf#L170) | Bridge service perimeters. | <code title="map(object({ spec_resources = optional(list(string)) status_resources = optional(list(string)) use_explicit_dry_run_spec = optional(bool, false) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [service_perimeters_regular](variables.tf#L180) | Regular service perimeters. | <code title="map(object({ spec = optional(object({ access_levels = optional(list(string)) resources = optional(list(string)) restricted_services = optional(list(string)) egress_policies = optional(list(string)) ingress_policies = optional(list(string)) vpc_accessible_services = optional(object({ allowed_services = list(string) enable_restriction = bool })) })) status = optional(object({ access_levels = optional(list(string)) resources = optional(list(string)) restricted_services = optional(list(string)) egress_policies = optional(list(string)) ingress_policies = optional(list(string)) vpc_accessible_services = optional(object({ allowed_services = list(string) enable_restriction = bool })) })) use_explicit_dry_run_spec = optional(bool, false) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [factories_config](variables.tf#L102) | Paths to folders that enable factory functionality. | <code title="object({ access_levels = optional(string) egress_policies = optional(string) ingress_policies = optional(string) })">object({…})</code> | | <code>{}</code> |
|
||||
| [iam](variables.tf#L113) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
||||
| [iam_bindings](variables.tf#L119) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code title="map(object({ members = list(string) role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [iam_bindings_additive](variables.tf#L134) | Individual additive IAM bindings. Keys are arbitrary. | <code title="map(object({ member = string role = string condition = optional(object({ expression = string title = string description = optional(string) })) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [ingress_policies](variables.tf#L149) | Ingress policy definitions that can be referenced in perimeters. | <code title="map(object({ from = object({ access_levels = optional(list(string), []) identity_type = optional(string) identities = optional(list(string)) resources = optional(list(string), []) }) to = object({ operations = optional(list(object({ method_selectors = optional(list(string)) permission_selectors = optional(list(string)) service_name = string })), []) resources = optional(list(string)) }) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [service_perimeters_bridge](variables.tf#L181) | Bridge service perimeters. | <code title="map(object({ spec_resources = optional(list(string)) status_resources = optional(list(string)) use_explicit_dry_run_spec = optional(bool, false) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
| [service_perimeters_regular](variables.tf#L191) | Regular service perimeters. | <code title="map(object({ spec = optional(object({ access_levels = optional(list(string)) resources = optional(list(string)) restricted_services = optional(list(string)) egress_policies = optional(list(string)) ingress_policies = optional(list(string)) vpc_accessible_services = optional(object({ allowed_services = list(string) enable_restriction = bool })) })) status = optional(object({ access_levels = optional(list(string)) resources = optional(list(string)) restricted_services = optional(list(string)) egress_policies = optional(list(string)) ingress_policies = optional(list(string)) vpc_accessible_services = optional(object({ allowed_services = list(string) enable_restriction = bool })) })) use_explicit_dry_run_spec = optional(bool, false) }))">map(object({…}))</code> | | <code>{}</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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: {}
|
Loading…
Reference in New Issue