From 4998f1d3766075306fd2709afea930c973564404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sat, 29 Jul 2023 20:07:21 +0200 Subject: [PATCH] Grant IAM rights to service identities in host project (#1542) * [module/project] Grant IAM rights to service identities based on used services in host project * [blueprints/factories/project-factory] enable granting IAM permissions in host VPC for service identities directly or by specifying services in use --- .../factories/project-factory/README.md | 14 ++- blueprints/factories/project-factory/main.tf | 58 +++++------ .../sample-data/projects/project.yaml | 10 ++ .../factories/project-factory/variables.tf | 24 +++-- modules/project/README.md | 35 +++++-- modules/project/outputs.tf | 9 ++ modules/project/shared-vpc.tf | 26 +++-- modules/project/sharedvpc-agent-iam.yaml | 97 +++++++++++++++++++ modules/project/variables.tf | 18 +++- .../project_factory/examples/example.yaml | 5 +- .../examples/shared-vpc-auto-grants.yaml | 42 ++++++++ 11 files changed, 283 insertions(+), 55 deletions(-) create mode 100644 modules/project/sharedvpc-agent-iam.yaml create mode 100644 tests/modules/project/examples/shared-vpc-auto-grants.yaml diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index c121d2b2..1025c825 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -76,7 +76,7 @@ module "projects" { service_identities_iam = try(each.value.service_identities_iam, {}) vpc = try(each.value.vpc, null) } -# tftest modules=7 resources=36 inventory=example.yaml +# tftest modules=7 resources=38 inventory=example.yaml ``` ### Projects configuration @@ -212,6 +212,16 @@ vpc: # Host project the project will be service project of host_project: fast-prod-net-spoke-0 + # [opt] Services for which set up the IAM in the host project + service_iam_grants: + - dataproc.googleapis.com + + # [opt] Roles to rant service project service identities in host project + service_identity_iam: + "roles/compute.networkUser": + - cloudservices + - container-engine + # [opt] Subnets in the host project where principals will be granted networkUser # in region/subnet-name => [principals] subnets_iam: @@ -248,7 +258,7 @@ vpc: | [service_identities_iam](variables.tf#L184) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | | [service_identities_iam_additive](variables.tf#L191) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | | [services](variables.tf#L198) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L205) | VPC configuration for the project. | object({…}) | | null | +| [vpc](variables.tf#L205) | VPC configuration for the project. | object({…}) | | {…} | ## Outputs diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf index f70684f9..9dbecf2f 100644 --- a/blueprints/factories/project-factory/main.tf +++ b/blueprints/factories/project-factory/main.tf @@ -15,6 +15,19 @@ */ locals { + _gke_config_service_identity_iam = { + "roles/compute.networkUser" = compact([ + var.vpc.gke_setup.enable_host_service_agent ? "container-engine" : null, + local.vpc_cloudservices ? "cloudservices" : null + ]) + "roles/compute.securityAdmin" = compact([ + var.vpc.gke_setup.enable_security_admin ? "container-engine" : null, + ]) + "roles/container.hostServiceAgentUser" = compact([ + var.vpc.gke_setup.enable_host_service_agent ? "container-engine" : null + ]) + } + _group_iam = { for r in local._group_iam_bindings : r => [ for k, v in var.group_iam : @@ -76,10 +89,10 @@ locals { ] } _vpc_subnet_bindings = ( - local.vpc.subnets_iam == null || local.vpc.host_project == null + var.vpc.subnets_iam == null || var.vpc.host_project == null ? [] : flatten([ - for subnet, members in local.vpc.subnets_iam : [ + for subnet, members in var.vpc.subnets_iam : [ for member in members : { region = split("/", subnet)[0] subnet = split("/", subnet)[1] @@ -131,19 +144,18 @@ locals { coalesce(var.labels, {}), coalesce(try(var.defaults.labels, {}), {}) ) services = distinct(concat(var.services, local._services)) - vpc = coalesce(var.vpc, { - host_project = null, gke_setup = null, subnets_iam = null - }) vpc_cloudservices = ( - local.vpc_gke_service_agent || + var.vpc.gke_setup.enable_host_service_agent || contains(var.services, "compute.googleapis.com") ) - vpc_gke_security_admin = coalesce( - try(local.vpc.gke_setup.enable_security_admin, null), false - ) - vpc_gke_service_agent = coalesce( - try(local.vpc.gke_setup.enable_host_service_agent, null), false - ) + + vpc_service_identity_iam = { + for role in setunion(keys(local._gke_config_service_identity_iam), keys(var.vpc.service_identity_iam)) : + role => setunion( + lookup(local._gke_config_service_identity_iam, role, []), + lookup(var.vpc.service_identity_iam, role, []), + ) + } vpc_subnet_bindings = { for binding in local._vpc_subnet_bindings : "${binding.subnet}:${binding.member}" => binding @@ -169,7 +181,7 @@ module "billing-alert" { module "dns" { source = "../../../modules/dns" for_each = toset(var.dns_zones) - project_id = coalesce(local.vpc.host_project, module.project.project_id) + project_id = coalesce(var.vpc.host_project, module.project.project_id) name = each.value zone_config = { domain = "${each.value}.${var.defaults.environment_dns_zone}" @@ -194,20 +206,10 @@ module "project" { service_encryption_key_ids = var.kms_service_agents services = local.services shared_vpc_service_config = var.vpc == null ? null : { - host_project = local.vpc.host_project + host_project = var.vpc.host_project # these are non-authoritative - service_identity_iam = { - "roles/compute.networkUser" = compact([ - local.vpc_gke_service_agent ? "container-engine" : null, - local.vpc_cloudservices ? "cloudservices" : null - ]) - "roles/compute.securityAdmin" = compact([ - local.vpc_gke_security_admin ? "container-engine" : null, - ]) - "roles/container.hostServiceAgentUser" = compact([ - local.vpc_gke_service_agent ? "container-engine" : null - ]) - } + service_identity_iam = local.vpc_service_identity_iam + service_iam_grants = var.vpc.service_iam_grants } } @@ -221,8 +223,8 @@ module "service-accounts" { resource "google_compute_subnetwork_iam_member" "default" { for_each = local.vpc_subnet_bindings - project = local.vpc.host_project - subnetwork = "projects/${local.vpc.host_project}/regions/${each.value.region}/subnetworks/${each.value.subnet}" + project = var.vpc.host_project + subnetwork = "projects/${var.vpc.host_project}/regions/${each.value.region}/subnetworks/${each.value.subnet}" region = each.value.region role = "roles/compute.networkUser" member = ( diff --git a/blueprints/factories/project-factory/sample-data/projects/project.yaml b/blueprints/factories/project-factory/sample-data/projects/project.yaml index d8cf982e..e5957c77 100644 --- a/blueprints/factories/project-factory/sample-data/projects/project.yaml +++ b/blueprints/factories/project-factory/sample-data/projects/project.yaml @@ -99,6 +99,16 @@ vpc: # Host project the project will be service project of host_project: fast-dev-net-spoke-0 + # [opt] Services for which set up the IAM in the host project + service_iam_grants: + - dataproc.googleapis.com + + # [opt] Roles to rant service project service identities in host project + service_identity_iam: + "roles/compute.networkUser": + - cloudservices + - container-engine + # [opt] Subnets in the host project where principals will be granted networkUser # in region/subnet-name => [principals] subnets_iam: diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index a2089bcf..f4dbc629 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -206,11 +206,23 @@ variable "vpc" { description = "VPC configuration for the project." type = object({ host_project = string - gke_setup = object({ - enable_security_admin = bool - enable_host_service_agent = bool - }) - subnets_iam = map(list(string)) + gke_setup = optional(object({ + enable_security_admin = optional(bool, false) + enable_host_service_agent = optional(bool, false) + }), {}) + service_iam_grants = optional(list(string), []) + service_identity_iam = optional(map(list(string)), {}) + subnets_iam = optional(map(list(string)), {}) }) - default = null + default = { + host_project = null + } + nullable = false + validation { + condition = var.vpc.host_project != null || ( + var.vpc.host_project == null && length(var.vpc.gke_setup) == 0 && length(var.vpc.service_iam_grants) == 0 && + length(var.vpc.service_identity_iam) == 0 && length(var.vpc.subnets_iam) == 0 + ) + error_message = "host_project is required if providing any additional configuration for vpc" + } } diff --git a/modules/project/README.md b/modules/project/README.md index b6f21193..a39df3ce 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -259,6 +259,30 @@ module "service-project" { # tftest modules=2 resources=8 inventory=shared-vpc.yaml ``` +The module allows also granting necessary permissions in host project to service identities by specifying which services will be used in service project in `grant_iam_for_services`. +```hcl +module "host-project" { + source = "./fabric/modules/project" + name = "my-host-project" + shared_vpc_host_config = { + enabled = true + } +} + +module "service-project" { + source = "./fabric/modules/project" + name = "my-service-project" + services = [ + "container.googleapis.com", + ] + shared_vpc_service_config = { + host_project = module.host-project.project_id + service_iam_grants = module.service-project.services + } +} +# tftest modules=2 resources=9 inventory=shared-vpc-auto-grants.yaml +``` + ## Organization Policies To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -577,7 +601,6 @@ output "compute_robot" { ``` - ## Files @@ -631,9 +654,9 @@ output "compute_robot" { | [service_perimeter_standard](variables.tf#L272) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | | [services](variables.tf#L278) | Service APIs to enable. | list(string) | | [] | | [shared_vpc_host_config](variables.tf#L284) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L293) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | -| [skip_delete](variables.tf#L303) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L309) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [shared_vpc_service_config](variables.tf#L293) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L315) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L321) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs @@ -645,6 +668,6 @@ output "compute_robot" { | [number](outputs.tf#L56) | Project number. | | | [project_id](outputs.tf#L75) | Project id. | | | [service_accounts](outputs.tf#L94) | Product robot service accounts in project. | | -| [sink_writer_identities](outputs.tf#L110) | Writer identities created for each sink. | | +| [services](outputs.tf#L110) | Service APIs to enabled in the project. | | +| [sink_writer_identities](outputs.tf#L119) | Writer identities created for each sink. | | - diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf index 81f20cdd..ae7bbc6e 100644 --- a/modules/project/outputs.tf +++ b/modules/project/outputs.tf @@ -107,6 +107,15 @@ output "service_accounts" { ] } +output "services" { + description = "Service APIs to enabled in the project." + value = var.services + depends_on = [ + google_project_service.project_services, + google_project_service_identity.jit_si, + ] +} + output "sink_writer_identities" { description = "Writer identities created for each sink." value = { diff --git a/modules/project/shared-vpc.tf b/modules/project/shared-vpc.tf index ee2d6b41..d728f42b 100644 --- a/modules/project/shared-vpc.tf +++ b/modules/project/shared-vpc.tf @@ -17,15 +17,25 @@ # tfdoc:file:description Shared VPC project-level configuration. locals { + _shared_vpc_agent_config = yamldecode(file("${path.module}/sharedvpc-agent-iam.yaml")) + _shared_vpc_agent_config_filtered = [ + for config in local._shared_vpc_agent_config : config + if contains(var.shared_vpc_service_config.service_iam_grants, config.service) + ] + _shared_vpc_agent_grants = flatten(flatten([ + for api in local._shared_vpc_agent_config_filtered : [ + for service, roles in api.agents : [ + for role in roles : { role = role, service = service } + ] + ] + ])) + # compute the host project IAM bindings for this project's service identities _svpc_service_iam = flatten([ - for role, services in local._svpc_service_identity_iam : [ + for role, services in var.shared_vpc_service_config.service_identity_iam : [ for service in services : { role = role, service = service } ] ]) - _svpc_service_identity_iam = coalesce( - local.svpc_service_config.service_identity_iam, {} - ) svpc_host_config = { enabled = coalesce( try(var.shared_vpc_host_config.enabled, null), false @@ -34,11 +44,9 @@ locals { try(var.shared_vpc_host_config.service_projects, null), [] ) } - svpc_service_config = coalesce(var.shared_vpc_service_config, { - host_project = null, service_identity_iam = {} - }) + svpc_service_iam = { - for b in local._svpc_service_iam : "${b.role}:${b.service}" => b + for b in setunion(local._svpc_service_iam, local._shared_vpc_agent_grants) : "${b.role}:${b.service}" => b } } @@ -59,7 +67,7 @@ resource "google_compute_shared_vpc_service_project" "service_projects" { resource "google_compute_shared_vpc_service_project" "shared_vpc_service" { provider = google-beta - count = local.svpc_service_config.host_project != null ? 1 : 0 + count = var.shared_vpc_service_config.host_project != null ? 1 : 0 host_project = var.shared_vpc_service_config.host_project service_project = local.project.project_id } diff --git a/modules/project/sharedvpc-agent-iam.yaml b/modules/project/sharedvpc-agent-iam.yaml new file mode 100644 index 00000000..3cb8ee3d --- /dev/null +++ b/modules/project/sharedvpc-agent-iam.yaml @@ -0,0 +1,97 @@ +# Copyright 2023 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. + +# Cloud Composer +# https://cloud.google.com/composer/docs/how-to/managing/configuring-shared-vpc#edit_permissions_for_the_composer_agent_service_account +- service: composer.googleapis.com + agents: + composer: + - roles/compute.networkUser + - roles/composer.sharedVpcAgent + +# Compute Engine +# TODO: identify docs +- service: compute.googleapis.com + agents: + cloudservices: + - roles/compute.networkUser + +# Google Kubernetes Engine +# https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc#enabling_and_granting_roles +- service: container.googleapis.com + agents: + container: + - roles/compute.networkUser + - roles/container.hostServiceAgentUser + - roles/compute.securityAdmin # to manage firewall rules + cloudservices: + - roles/compute.networkUser + +# Dataflow +# https://cloud.google.com/dataflow/docs/guides/specifying-networks#shared +- service: dataflow.googleapis.com + agents: + dataflow: + - roles/compute.networkUser + +# Cloud Data Fusion +# https://cloud.google.com/data-fusion/docs/how-to/create-private-ip#shared-vpc-network_1 +- service: datafusion.googleapis.com + agents: + datafusion: + - roles/compute.networkUser + dataproc: + - roles/compute.networkUser + +# Dataproc +# https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/network#create_a_cluster_that_uses_a_network_in_another_project +- service: dataproc.googleapis.com + agents: + dataproc: + - roles/compute.networkUser + cloudservices: + - roles/compute.networkUser + +# Change Data Capture | Datastream +# https://cloud.google.com/datastream/docs/create-a-private-connectivity-configuration +- service: datastream.googleapis.com + agents: + datastream: + - roles/compute.networkAdmin + +# Cloud Functions +# For shared connectors in host project +# https://cloud.google.com/functions/docs/networking/shared-vpc-host-project +- service: cloudfunctions.googleapis.com + agents: + cloudfunctions: + - roles/vpcaccess.user + +# Cloud Run +# For shared connectors in host project +# https://cloud.google.com/run/docs/configuring/shared-vpc-host-project +- service: run.googleapis.com + agents: + run: + - roles/vpcaccess.user + +# Cloud Run / Cloud Functions +# For connectors in service project +# https://cloud.google.com/functions/docs/networking/shared-vpc-service-projects#grant-permissions +- service: vpcaccess.googleapis.com + agents: + vpcaccess: + - roles/compute.networkUser + cloudservices: + - roles/compute.networkUser diff --git a/modules/project/variables.tf b/modules/project/variables.tf index bbd9fb60..eb6b5da7 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -292,12 +292,24 @@ variable "shared_vpc_host_config" { variable "shared_vpc_service_config" { description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." - # the list of valid service identities is in service-accounts.tf + # the list of valid service identities is in service-agents.yaml type = object({ host_project = string - service_identity_iam = optional(map(list(string))) + service_identity_iam = optional(map(list(string)), {}) + service_iam_grants = optional(list(string), []) }) - default = null + default = { + host_project = null + } + nullable = false + validation { + condition = var.shared_vpc_service_config.host_project != null || ( + var.shared_vpc_service_config.host_project == null && + length(var.shared_vpc_service_config.service_iam_grants) == 0 && + length(var.shared_vpc_service_config.service_iam_grants) == 0 + ) + error_message = "You need to provide host_project when providing service_identity_iam or service_iam_grants" + } } variable "skip_delete" { diff --git a/tests/blueprints/factories/project_factory/examples/example.yaml b/tests/blueprints/factories/project_factory/examples/example.yaml index ee4a1b48..82c6dd53 100644 --- a/tests/blueprints/factories/project_factory/examples/example.yaml +++ b/tests/blueprints/factories/project_factory/examples/example.yaml @@ -170,6 +170,9 @@ values: condition: [] project: fast-dev-net-spoke-0 role: roles/compute.securityAdmin + module.projects["project"].module.project.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:dataproc"]: + project: fast-dev-net-spoke-0 + role: roles/compute.networkUser module.projects["project"].module.project.google_kms_crypto_key_iam_member.service_identity_cmek["compute.key1"]: condition: [] crypto_key_id: key1 @@ -245,7 +248,7 @@ counts: google_org_policy_policy: 3 google_project: 1 google_project_iam_binding: 3 - google_project_iam_member: 2 + google_project_iam_member: 4 google_project_service: 8 google_service_account: 2 google_storage_project_service_account: 1 diff --git a/tests/modules/project/examples/shared-vpc-auto-grants.yaml b/tests/modules/project/examples/shared-vpc-auto-grants.yaml new file mode 100644 index 00000000..22661239 --- /dev/null +++ b/tests/modules/project/examples/shared-vpc-auto-grants.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 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.host-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: my-host-project + module.host-project.google_project.project[0]: + project_id: my-host-project + module.service-project.google_compute_shared_vpc_service_project.shared_vpc_service[0]: + host_project: my-host-project + service_project: my-service-project + module.service-project.google_project.project[0]: + project_id: my-service-project + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:cloudservices"]: + condition: [] + project: my-host-project + role: roles/compute.networkUser + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:container"]: + condition: [] + project: my-host-project + role: roles/compute.networkUser + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/container.hostServiceAgentUser:container"]: + condition: [] + project: my-host-project + role: roles/container.hostServiceAgentUser + +counts: + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 1 + google_project: 2 + google_project_iam_member: 4