From bc1b8b1dec3830f184b3892c9aa41de17c581b41 Mon Sep 17 00:00:00 2001 From: Amanda Karina Lopes de Oliveira Date: Fri, 5 May 2023 13:51:19 -0300 Subject: [PATCH] feat!: adds support to multiple service projects and Shared VPC (#115) --- docs/upgrading_to_v7.0.md | 64 ++++++++ examples/secure_cloud_run_standalone/main.tf | 14 +- .../secure_cloud_run_standalone/outputs.tf | 12 +- modules/secure-cloud-run/main.tf | 3 +- .../README.md | 18 ++- .../main.tf | 142 +++++++++--------- .../network.tf | 39 ++++- .../outputs.tf | 70 +++++---- .../private_service_connect.tf | 9 +- .../service_perimeter.tf | 44 +++++- .../variables.tf | 26 +++- modules/secure-serverless-harness/versions.tf | 46 ++++++ .../README.md | 1 + .../firewall.tf | 53 +++---- .../iam.tf | 4 + .../network.tf | 0 .../outputs.tf | 0 .../variables.tf | 10 ++ .../versions.tf | 4 +- modules/service-project-factory/main.tf | 96 ++++++++++++ modules/service-project-factory/outputs.tf | 45 ++++++ modules/service-project-factory/variables.tf | 62 ++++++++ .../versions.tf | 4 +- .../secure_cloud_run/secure_cloud_run_test.go | 126 ++++++++++++++++ .../secure_cloud_run_standalone_test.go | 126 ++++++++++++++++ 25 files changed, 852 insertions(+), 166 deletions(-) create mode 100644 docs/upgrading_to_v7.0.md rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/README.md (90%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/main.tf (55%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/network.tf (61%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/outputs.tf (55%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/private_service_connect.tf (81%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/service_perimeter.tf (83%) rename modules/{secure-cloud-serverless-harness => secure-serverless-harness}/variables.tf (90%) create mode 100644 modules/secure-serverless-harness/versions.tf rename modules/{secure-cloud-serverless-net => secure-serverless-net}/README.md (97%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/firewall.tf (88%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/iam.tf (95%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/network.tf (100%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/outputs.tf (100%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/variables.tf (89%) rename modules/{secure-cloud-serverless-net => secure-serverless-net}/versions.tf (94%) create mode 100644 modules/service-project-factory/main.tf create mode 100644 modules/service-project-factory/outputs.tf create mode 100644 modules/service-project-factory/variables.tf rename modules/{secure-cloud-serverless-harness => service-project-factory}/versions.tf (94%) diff --git a/docs/upgrading_to_v7.0.md b/docs/upgrading_to_v7.0.md new file mode 100644 index 00000000..3bbdc18c --- /dev/null +++ b/docs/upgrading_to_v7.0.md @@ -0,0 +1,64 @@ +# Upgrading to v7.0 + +The v7.0 release contains backwards-incompatible +changes due to renaming the sub-modules name. + +## secure-serverless-net + +The module was rename from `secure-cloud-serverless-net` to `secure-serverless-net`. +The required variable`serverless_type` was also added to allow re-use from Cloud Functions (2nd Gen). + +```diff +module "cloud_run_network" { +- source = "../secure-cloud-serverless-net" ++ source = "../secure-serverless-net" + + connector_name = var.connector_name + subnet_name = var.subnet_name + location = var.location + vpc_project_id = var.vpc_project_id + serverless_project_id = var.serverless_project_id + shared_vpc_name = var.shared_vpc_name + connector_on_host_project = false + ip_cidr_range = var.ip_cidr_range + create_subnet = var.create_subnet + resource_names_suffix = var.resource_names_suffix ++ serverless_type = "CLOUD_RUN" + serverless_service_identity_email = google_project_service_identity.serverless_sa.email +} +``` + +## secure-serverless-harness + +The module was rename from `secure-cloud-serverless-harness` to `secure-serverless-harness`. +The`serverless_project_name` variable was changed to accept more than one name, to create +one or more service projects. + +```diff +module "secure_harness" { +- source = "../../modules/secure-cloud-serverless-harness" ++ source = "../../modules/secure-serverless-harness" + billing_account = var.billing_account + security_project_name = "prj-kms-secure-cloud-run" +- serverless_project_name = "prj-secure-cloud-run" ++ serverless_project_names = ["prj-secure-cloud-run"] + org_id = var.org_id + parent_folder_id = var.parent_folder_id + serverless_folder_suffix = random_id.random_folder_suffix.hex + serverless_service_identity_email = google_project_service_identity.serverless_sa.email + region = local.region + location = local.location + vpc_name = "vpc-secure-cloud-run" + subnet_ip = "10.0.0.0/28" + private_service_connect_ip = "10.3.0.5" + create_access_context_manager_access_policy = var.create_access_context_manager_access_policy + access_context_manager_policy_id = var.access_context_manager_policy_id + access_level_members = var.access_level_members + key_name = "key-secure-artifact-registry" + keyring_name = "krg-secure-artifact-registry" + prevent_destroy = false + artifact_registry_repository_name = local.repository_name + egress_policies = var.egress_policies + ingress_policies = var.ingress_policies + serverless_type = "CLOUD_RUN" +``` diff --git a/examples/secure_cloud_run_standalone/main.tf b/examples/secure_cloud_run_standalone/main.tf index 242e066a..8113a747 100644 --- a/examples/secure_cloud_run_standalone/main.tf +++ b/examples/secure_cloud_run_standalone/main.tf @@ -27,10 +27,10 @@ resource "random_id" "random_folder_suffix" { } module "secure_harness" { - source = "../../modules/secure-cloud-serverless-harness" + source = "../../modules/secure-serverless-harness" billing_account = var.billing_account security_project_name = "prj-kms-secure-cloud-run" - serverless_project_name = "prj-secure-cloud-run" + serverless_project_names = ["prj-secure-cloud-run"] org_id = var.org_id parent_folder_id = var.parent_folder_id serverless_folder_suffix = random_id.random_folder_suffix.hex @@ -65,18 +65,18 @@ module "secure_cloud_run" { source = "../../modules/secure-cloud-run" location = local.location region = local.region - serverless_project_id = module.secure_harness.serverless_project_id - vpc_project_id = module.secure_harness.serverless_project_id + serverless_project_id = module.secure_harness.serverless_project_ids[0] + vpc_project_id = module.secure_harness.network_project_id[0] kms_project_id = module.secure_harness.security_project_id key_name = "key-secure-cloud-run" keyring_name = "krg-secure-cloud-run" service_name = "srv-secure-cloud-run" image = "${local.location}-docker.pkg.dev/${module.secure_harness.security_project_id}/${module.secure_harness.artifact_registry_repository_name}/hello:latest" - cloud_run_sa = module.secure_harness.service_account_email + cloud_run_sa = module.secure_harness.service_account_email[module.secure_harness.serverless_project_ids[0]] connector_name = "con-secure-cloud-run" - subnet_name = module.secure_harness.service_subnet + subnet_name = module.secure_harness.service_subnet[0] create_subnet = false - shared_vpc_name = module.secure_harness.service_vpc.network_name + shared_vpc_name = module.secure_harness.service_vpc[0].network_name ip_cidr_range = "10.0.0.0/28" prevent_destroy = false artifact_registry_repository_location = local.location diff --git a/examples/secure_cloud_run_standalone/outputs.tf b/examples/secure_cloud_run_standalone/outputs.tf index ded60f39..9e7a14b1 100644 --- a/examples/secure_cloud_run_standalone/outputs.tf +++ b/examples/secure_cloud_run_standalone/outputs.tf @@ -15,12 +15,12 @@ */ output "serverless_project_id" { - value = module.secure_harness.serverless_project_id + value = module.secure_harness.serverless_project_ids[0] description = "The serverless project id." } output "serverless_project_number" { - value = module.secure_harness.serverless_project_number + value = module.secure_harness.serverless_project_numbers[module.secure_harness.serverless_project_ids[0]] description = "The serverless project number." } @@ -35,22 +35,22 @@ output "security_project_number" { } output "service_account_email" { - value = module.secure_harness.service_account_email + value = module.secure_harness.service_account_email[module.secure_harness.serverless_project_ids[0]] description = "The service account email created to be used by Cloud Run." } output "service_vpc_self_link" { - value = module.secure_harness.service_vpc.network.self_link + value = module.secure_harness.service_vpc[0].network.self_link description = "The Network self-link created in harness." } output "service_vpc_name" { - value = module.secure_harness.service_vpc.network_name + value = module.secure_harness.service_vpc[0].network_name description = "The Network self-link created in harness." } output "service_vpc_subnet_name" { - value = module.secure_harness.service_subnet + value = module.secure_harness.service_subnet[0] description = "The sub-network name created in harness." } diff --git a/modules/secure-cloud-run/main.tf b/modules/secure-cloud-run/main.tf index ccb9c113..47756ca8 100644 --- a/modules/secure-cloud-run/main.tf +++ b/modules/secure-cloud-run/main.tf @@ -44,7 +44,7 @@ module "vpc_project_apis" { } module "cloud_run_network" { - source = "../secure-cloud-serverless-net" + source = "../secure-serverless-net" connector_name = var.connector_name subnet_name = var.subnet_name @@ -56,6 +56,7 @@ module "cloud_run_network" { ip_cidr_range = var.ip_cidr_range create_subnet = var.create_subnet resource_names_suffix = var.resource_names_suffix + serverless_type = "CLOUD_RUN" serverless_service_identity_email = google_project_service_identity.serverless_sa.email diff --git a/modules/secure-cloud-serverless-harness/README.md b/modules/secure-serverless-harness/README.md similarity index 90% rename from modules/secure-cloud-serverless-harness/README.md rename to modules/secure-serverless-harness/README.md index c58dad57..f8aa268e 100644 --- a/modules/secure-cloud-serverless-harness/README.md +++ b/modules/secure-serverless-harness/README.md @@ -70,6 +70,7 @@ module "secure_cloud_run_harness" { | key\_rotation\_period | Period of key rotation in seconds. Default value is equivalent to 30 days. | `string` | `"2592000s"` | no | | keyring\_name | Keyring name. | `string` | n/a | yes | | location | The location where resources are going to be deployed. | `string` | n/a | yes | +| network\_project\_name | The name to give the shared vpc project. | `string` | `""` | no | | org\_id | The organization ID. | `string` | n/a | yes | | owners | List of comma-separated owners for each key declared in set\_owners\_for. | `list(string)` | `[]` | no | | parent\_folder\_id | The ID of a folder to host the infrastructure created in this module. | `string` | `""` | no | @@ -78,10 +79,11 @@ module "secure_cloud_run_harness" { | region | The region in which the subnetwork will be created. | `string` | n/a | yes | | security\_project\_name | The name to give the security project. | `string` | n/a | yes | | serverless\_folder\_suffix | The suffix to be concat in the Serverless folder name fldr-serverless-. | `string` | `""` | no | -| serverless\_project\_name | The name to give the Cloud Run project. | `string` | n/a | yes | +| serverless\_project\_names | The name to give the Cloud Serverless project. | `list(string)` | n/a | yes | | serverless\_type | The type of resource to be used. It supports only CLOUD\_RUN or CLOUD\_FUNCTION | `string` | n/a | yes | -| service\_account\_project\_roles | Common roles to apply to the Cloud Run service account in the serverless project. | `list(string)` | `[]` | no | +| service\_account\_project\_roles | Common roles to apply to the Cloud Serverless service account in the serverless project. | `map(list(string))` | `{}` | no | | subnet\_ip | The CDIR IP range of the subnetwork. | `string` | n/a | yes | +| use\_shared\_vpc | Defines if the network created will be a single or shared vpc. | `bool` | `false` | no | | vpc\_name | The name of the network. | `string` | n/a | yes | ## Outputs @@ -90,17 +92,19 @@ module "secure_cloud_run_harness" { |------|-------------| | artifact\_registry\_repository\_id | The Artifact Registry Repository full identifier where the images should be stored. | | artifact\_registry\_repository\_name | The Artifact Registry Repository last part of the repository name where the images should be stored. | -| cloud\_run\_service\_identity\_email | The Cloud Run Service Identity email. | +| cloud\_serverless\_service\_identity\_email | The Cloud Run Service Identity email. | +| cloudfunction\_source\_bucket | Cloud Function Source Bucket. | +| network\_project\_id | Project ID of the project created to host the Cloud Run Network. | | restricted\_access\_level\_name | Access level name. | | restricted\_service\_perimeter\_name | Service Perimeter name. | | security\_project\_id | Project ID of the project created for KMS and Artifact Register. | | security\_project\_number | Project number of the project created for KMS and Artifact Register. | | serverless\_folder\_id | The folder created to alocate Serverless infra. | -| serverless\_project\_id | Project ID of the project created to deploy Cloud Run. | -| serverless\_project\_number | Project number of the project created to deploy Cloud Run. | -| service\_account\_email | The email of the Service Account created to be used by Cloud Run. | +| serverless\_project\_ids | Project ID of the projects created to deploy Cloud Run. | +| serverless\_project\_numbers | Project number of the projects created to deploy Cloud Run. | +| service\_account\_email | The email of the Service Account created to be used by Cloud Serverless. | | service\_subnet | The sub-network name created in harness. | -| service\_vpc | The network created for Cloud Run. | +| service\_vpc | The network created for Cloud Serverless. | diff --git a/modules/secure-cloud-serverless-harness/main.tf b/modules/secure-serverless-harness/main.tf similarity index 55% rename from modules/secure-cloud-serverless-harness/main.tf rename to modules/secure-serverless-harness/main.tf index bef51058..316fad1b 100644 --- a/modules/secure-cloud-serverless-harness/main.tf +++ b/modules/secure-serverless-harness/main.tf @@ -15,23 +15,33 @@ */ locals { - api = var.serverless_type == "CLOUD_RUN" ? "run" : "cloudfunctions" - serverless_apis = [ + api = var.serverless_type == "CLOUD_FUNCTION" ? ["cloudfunctions.googleapis.com", "cloudbuild.googleapis.com", "eventarc.googleapis.com", "eventarcpublishing.googleapis.com"] : [] + serverless_apis = concat([ "vpcaccess.googleapis.com", "compute.googleapis.com", "container.googleapis.com", "artifactregistry.googleapis.com", - "${local.api}.googleapis.com", + "run.googleapis.com", "cloudkms.googleapis.com", "dns.googleapis.com" - ] + ], local.api) kms_apis = [ "cloudkms.googleapis.com", "artifactregistry.googleapis.com" ] - decrypters = join(",", concat(["serviceAccount:${google_project_service_identity.artifact_sa.email}"], var.decrypters)) - encrypters = join(",", concat(["serviceAccount:${google_project_service_identity.artifact_sa.email}"], var.encrypters)) + network_apis = [ + "vpcaccess.googleapis.com", + "compute.googleapis.com", + "dns.googleapis.com" + ] + + network_project_id = var.use_shared_vpc ? module.network_project[0].project_id : "" + + eventarc_identities = [for project in module.serverless_project : "serviceAccount:${project.services_identities["eventarc"]}"] + gcs_identities = [for project in module.serverless_project : "serviceAccount:${project.services_identities["gcs"]}"] + decrypters = join(",", concat(["serviceAccount:${google_project_service_identity.artifact_sa.email}"], local.eventarc_identities, local.gcs_identities, var.decrypters)) + encrypters = join(",", concat(["serviceAccount:${google_project_service_identity.artifact_sa.email}"], local.eventarc_identities, local.gcs_identities, var.encrypters)) } @@ -40,82 +50,45 @@ resource "google_folder" "fld_serverless" { parent = var.parent_folder_id == "" ? "organizations/${var.org_id}" : "folders/${var.parent_folder_id}" } -module "security_project" { +module "network_project" { + count = var.use_shared_vpc ? 1 : 0 source = "terraform-google-modules/project-factory/google" - version = "~> 13.0" + version = "~> 14.2" random_project_id = "true" - activate_apis = local.kms_apis - name = var.security_project_name + activate_apis = local.network_apis + name = var.network_project_name org_id = var.org_id billing_account = var.billing_account folder_id = google_folder.fld_serverless.name + + enable_shared_vpc_host_project = true } -module "serverless_project" { +module "security_project" { source = "terraform-google-modules/project-factory/google" - version = "~> 13.0" + version = "~> 14.2" random_project_id = "true" - activate_apis = local.serverless_apis - name = var.serverless_project_name + activate_apis = local.kms_apis + name = var.security_project_name org_id = var.org_id billing_account = var.billing_account folder_id = google_folder.fld_serverless.name } -module "service_accounts" { - source = "terraform-google-modules/service-accounts/google" - version = "~> 3.0" - project_id = module.serverless_project.project_id - prefix = "sa" - names = ["serverless-${local.api}"] - - depends_on = [ - time_sleep.wait_90_seconds - ] -} - -resource "google_project_iam_member" "cloud_run_sa_roles" { - for_each = toset(var.service_account_project_roles) - project = module.serverless_project.project_id - role = each.value - member = module.service_accounts.iam_email - - depends_on = [ - time_sleep.wait_90_seconds - ] -} - -resource "google_project_service_identity" "serverless_sa" { - provider = google-beta - - project = module.serverless_project.project_id - service = "${local.api}.googleapis.com" - - depends_on = [ - time_sleep.wait_90_seconds - ] -} +module "serverless_project" { + source = "../service-project-factory" -resource "google_service_account_iam_member" "identity_service_account_user" { - service_account_id = module.service_accounts.service_account.id - role = "roles/iam.serviceAccountUser" - member = "serviceAccount:${google_project_service_identity.serverless_sa.email}" + for_each = toset(var.serverless_project_names) - depends_on = [ - time_sleep.wait_90_seconds - ] + billing_account = var.billing_account + serverless_type = var.serverless_type + org_id = var.org_id + activate_apis = local.serverless_apis + folder_name = google_folder.fld_serverless.name + project_name = each.value + service_account_project_roles = length(var.service_account_project_roles) > 0 ? var.service_account_project_roles[each.value] : [] } -resource "google_project_service_identity" "artifact_sa" { - provider = google-beta - - project = module.security_project.project_id - service = "artifactregistry.googleapis.com" - - depends_on = [ - time_sleep.wait_90_seconds - ] -} resource "google_artifact_registry_repository" "repo" { project = module.security_project.project_id @@ -126,19 +99,20 @@ resource "google_artifact_registry_repository" "repo" { kms_key_name = module.artifact_registry_kms.keys[var.key_name] depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } resource "google_artifact_registry_repository_iam_member" "member" { + for_each = module.serverless_project project = module.security_project.project_id location = var.location repository = google_artifact_registry_repository.repo.repository_id role = "roles/artifactregistry.reader" - member = "serviceAccount:${google_project_service_identity.serverless_sa.email}" + member = "serviceAccount:${each.value.cloud_serverless_service_identity_email}" depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -161,6 +135,38 @@ module "artifact_registry_kms" { key_protection_level = var.key_protection_level depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds + ] +} + +resource "google_project_service_identity" "artifact_sa" { + provider = google-beta + + project = module.security_project.project_id + service = "artifactregistry.googleapis.com" + + depends_on = [ + time_sleep.wait_180_seconds + ] +} + +module "cloudfunction_source_bucket" { + for_each = var.serverless_type == "CLOUD_RUN" ? {} : module.serverless_project + source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" + version = "~>3.4" + + project_id = each.value.project_id + name = "bkt-${var.location}-${each.value.project_number}-cfv2-zip-files" + location = var.location + storage_class = "REGIONAL" + force_destroy = true + + encryption = { + default_kms_key_name = module.artifact_registry_kms.keys[var.key_name] + } + + depends_on = [ + module.artifact_registry_kms, + time_sleep.wait_180_seconds ] } diff --git a/modules/secure-cloud-serverless-harness/network.tf b/modules/secure-serverless-harness/network.tf similarity index 61% rename from modules/secure-cloud-serverless-harness/network.tf rename to modules/secure-serverless-harness/network.tf index 506d8414..81e65e94 100644 --- a/modules/secure-cloud-serverless-harness/network.tf +++ b/modules/secure-serverless-harness/network.tf @@ -15,15 +15,19 @@ */ locals { - network_name = trimprefix(var.vpc_name, "vpc-") == var.vpc_name ? var.vpc_name : "vpc-${var.vpc_name}" + network_name = startswith(var.vpc_name, "vpc-") ? var.vpc_name : "vpc-${var.vpc_name}" + + services_projects = var.use_shared_vpc ? { for key, project in module.serverless_project : key => project.project_id } : {} + network_projects = var.use_shared_vpc ? { for key, project in module.network_project : key => project.project_id } : { for key, project in module.serverless_project : key => project.project_id } } module "network" { + for_each = local.network_projects source = "terraform-google-modules/network/google" - version = "~> 6.0" - project_id = module.serverless_project.project_id + version = "~> 7.0" + project_id = each.value network_name = local.network_name - shared_vpc_host = "false" + shared_vpc_host = var.use_shared_vpc delete_default_internet_gateway_routes = "true" subnets = [ @@ -68,18 +72,39 @@ module "network" { ports = ["443"] }] - ranges = [module.private_service_connect.private_service_connect_ip] + ranges = [var.private_service_connect_ip] target_tags = ["allow-google-apis", "vpc-connector"] } ] + depends_on = [ + module.network_project, + module.serverless_project, + time_sleep.wait_180_seconds + ] +} + +resource "google_compute_shared_vpc_service_project" "shared_vpc_attachment" { + for_each = local.services_projects + + host_project = module.network[0].project_id + service_project = each.value + depends_on = [ + module.serverless_project, + time_sleep.wait_180_seconds + ] } resource "google_dns_policy" "default_policy" { - project = module.serverless_project.project_id + for_each = module.network + + project = each.value.project_id name = "dns-default-policy" enable_inbound_forwarding = var.dns_enable_inbound_forwarding enable_logging = var.dns_enable_logging networks { - network_url = module.network.network_self_link + network_url = each.value.network_self_link } + depends_on = [ + time_sleep.wait_180_seconds + ] } diff --git a/modules/secure-cloud-serverless-harness/outputs.tf b/modules/secure-serverless-harness/outputs.tf similarity index 55% rename from modules/secure-cloud-serverless-harness/outputs.tf rename to modules/secure-serverless-harness/outputs.tf index 28d300c9..d097ab57 100644 --- a/modules/secure-cloud-serverless-harness/outputs.tf +++ b/modules/secure-serverless-harness/outputs.tf @@ -19,25 +19,34 @@ output "serverless_folder_id" { description = "The folder created to alocate Serverless infra." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } -output "serverless_project_id" { - value = module.serverless_project.project_id - description = "Project ID of the project created to deploy Cloud Run." +output "network_project_id" { + value = [for network in module.network : network.project_id] + description = "Project ID of the project created to host the Cloud Run Network." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } -output "serverless_project_number" { - value = module.serverless_project.project_number - description = "Project number of the project created to deploy Cloud Run." +output "serverless_project_ids" { + value = [for project in module.serverless_project : project.project_id] + description = "Project ID of the projects created to deploy Cloud Run." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds + ] +} + +output "serverless_project_numbers" { + value = { for project in module.serverless_project : project.project_id => project.project_number } + description = "Project number of the projects created to deploy Cloud Run." + + depends_on = [ + time_sleep.wait_180_seconds ] } @@ -46,7 +55,7 @@ output "security_project_id" { description = "Project ID of the project created for KMS and Artifact Register." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -55,34 +64,34 @@ output "security_project_number" { description = "Project number of the project created for KMS and Artifact Register." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } output "service_account_email" { - value = module.service_accounts.email - description = "The email of the Service Account created to be used by Cloud Run." + value = { for project in module.serverless_project : project.project_id => project.service_account_email } + description = "The email of the Service Account created to be used by Cloud Serverless." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } output "service_vpc" { - value = module.network.network - description = "The network created for Cloud Run." + value = [for network in module.network : network.network] + description = "The network created for Cloud Serverless." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } output "service_subnet" { - value = module.network.subnets_names[0] + value = [for network in module.network : network.subnets_names[0]] description = "The sub-network name created in harness." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -91,7 +100,7 @@ output "artifact_registry_repository_id" { description = "The Artifact Registry Repository full identifier where the images should be stored." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -100,16 +109,16 @@ output "artifact_registry_repository_name" { description = "The Artifact Registry Repository last part of the repository name where the images should be stored." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } -output "cloud_run_service_identity_email" { - value = google_project_service_identity.serverless_sa.email +output "cloud_serverless_service_identity_email" { + value = { for project in module.serverless_project : project.project_id => project.cloud_serverless_service_identity_email } description = "The Cloud Run Service Identity email." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -118,7 +127,7 @@ output "restricted_service_perimeter_name" { description = "Service Perimeter name." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds ] } @@ -127,6 +136,15 @@ output "restricted_access_level_name" { description = "Access level name." depends_on = [ - time_sleep.wait_90_seconds + time_sleep.wait_180_seconds + ] +} + +output "cloudfunction_source_bucket" { + value = var.serverless_type == "CLOUD_RUN" ? {} : { for bucket in module.cloudfunction_source_bucket : bucket.bucket.project => bucket.bucket } + description = "Cloud Function Source Bucket." + + depends_on = [ + time_sleep.wait_180_seconds ] } diff --git a/modules/secure-cloud-serverless-harness/private_service_connect.tf b/modules/secure-serverless-harness/private_service_connect.tf similarity index 81% rename from modules/secure-cloud-serverless-harness/private_service_connect.tf rename to modules/secure-serverless-harness/private_service_connect.tf index 7a42a8fe..df49ac3e 100644 --- a/modules/secure-cloud-serverless-harness/private_service_connect.tf +++ b/modules/secure-serverless-harness/private_service_connect.tf @@ -15,10 +15,15 @@ */ module "private_service_connect" { + for_each = module.network + source = "terraform-google-modules/network/google//modules/private-service-connect" version = "~> 6.0" - project_id = module.serverless_project.project_id - network_self_link = module.network.network_self_link + project_id = each.value.project_id + network_self_link = each.value.network_self_link private_service_connect_ip = var.private_service_connect_ip forwarding_rule_target = "vpc-sc" + depends_on = [ + time_sleep.wait_180_seconds + ] } diff --git a/modules/secure-cloud-serverless-harness/service_perimeter.tf b/modules/secure-serverless-harness/service_perimeter.tf similarity index 83% rename from modules/secure-cloud-serverless-harness/service_perimeter.tf rename to modules/secure-serverless-harness/service_perimeter.tf index e0e0c439..7089e282 100644 --- a/modules/secure-cloud-serverless-harness/service_perimeter.tf +++ b/modules/secure-serverless-harness/service_perimeter.tf @@ -19,7 +19,10 @@ locals { access_level_name = "alp_${local.prefix}_members_${random_id.random_access_level_suffix.hex}" perimeter_name = "sp_${local.prefix}_perimeter_${random_id.random_access_level_suffix.hex}" access_context_manager_policy_id = var.create_access_context_manager_access_policy ? google_access_context_manager_access_policy.access_policy[0].id : var.access_context_manager_policy_id - + access_level_members = concat(var.access_level_members, + [for project in module.serverless_project : "serviceAccount:${project.services_identities["cloudbuild"]}"], + [for project in module.serverless_project : "serviceAccount:${project.services_identities["gcs"]}"] + ) } resource "random_id" "random_access_level_suffix" { @@ -42,7 +45,7 @@ module "access_level_members" { description = "${local.prefix} Access Level" policy = local.access_context_manager_policy_id name = local.access_level_name - members = var.access_level_members + members = local.access_level_members } module "regular_service_perimeter" { @@ -183,21 +186,48 @@ module "regular_service_perimeter" { } resource "google_access_context_manager_service_perimeter_resource" "service_perimeter_serverless_resource" { + for_each = module.serverless_project perimeter_name = "accessPolicies/${local.access_context_manager_policy_id}/servicePerimeters/${module.regular_service_perimeter.perimeter_name}" - resource = "projects/${module.serverless_project.project_number}" + resource = "projects/${each.value.project_number}" + depends_on = [ + module.serverless_project, + module.security_project, + module.network_project, + module.regular_service_perimeter + ] } resource "google_access_context_manager_service_perimeter_resource" "service_perimeter_security_resource" { perimeter_name = "accessPolicies/${local.access_context_manager_policy_id}/servicePerimeters/${module.regular_service_perimeter.perimeter_name}" resource = "projects/${module.security_project.project_number}" + depends_on = [ + module.serverless_project, + module.security_project, + module.network_project, + module.regular_service_perimeter + ] +} + +resource "google_access_context_manager_service_perimeter_resource" "service_perimeter_network_resource" { + count = var.use_shared_vpc ? 1 : 0 + perimeter_name = "accessPolicies/${local.access_context_manager_policy_id}/servicePerimeters/${module.regular_service_perimeter.perimeter_name}" + resource = "projects/${module.network_project[0].project_number}" + depends_on = [ + module.serverless_project, + module.security_project, + module.network_project, + module.regular_service_perimeter + ] } -resource "time_sleep" "wait_90_seconds" { +resource "time_sleep" "wait_180_seconds" { depends_on = [ google_access_context_manager_service_perimeter_resource.service_perimeter_security_resource, - google_access_context_manager_service_perimeter_resource.service_perimeter_serverless_resource + google_access_context_manager_service_perimeter_resource.service_perimeter_serverless_resource, + google_access_context_manager_service_perimeter_resource.service_perimeter_network_resource, + module.access_level_members ] - create_duration = "90s" - destroy_duration = "90s" + create_duration = "180s" + destroy_duration = "180s" } diff --git a/modules/secure-cloud-serverless-harness/variables.tf b/modules/secure-serverless-harness/variables.tf similarity index 90% rename from modules/secure-cloud-serverless-harness/variables.tf rename to modules/secure-serverless-harness/variables.tf index fcf993f8..be358717 100644 --- a/modules/secure-cloud-serverless-harness/variables.tf +++ b/modules/secure-serverless-harness/variables.tf @@ -22,6 +22,10 @@ variable "billing_account" { variable "serverless_type" { description = "The type of resource to be used. It supports only CLOUD_RUN or CLOUD_FUNCTION" type = string + validation { + condition = contains(["CLOUD_RUN", "CLOUD_FUNCTION"], var.serverless_type) + error_message = "unsupported value for serverless_type" + } } variable "security_project_name" { @@ -29,9 +33,15 @@ variable "security_project_name" { type = string } -variable "serverless_project_name" { - description = "The name to give the Cloud Run project." +variable "network_project_name" { + description = "The name to give the shared vpc project." type = string + default = "" +} + +variable "serverless_project_names" { + description = "The name to give the Cloud Serverless project." + type = list(string) } variable "org_id" { @@ -63,6 +73,12 @@ variable "create_access_context_manager_access_policy" { default = false } +variable "use_shared_vpc" { + description = "Defines if the network created will be a single or shared vpc." + type = bool + default = false +} + variable "access_level_members" { description = "The list of additional members who will be in the access level." type = list(string) @@ -107,9 +123,9 @@ variable "private_service_connect_ip" { } variable "service_account_project_roles" { - type = list(string) - description = "Common roles to apply to the Cloud Run service account in the serverless project." - default = [] + type = map(list(string)) + description = "Common roles to apply to the Cloud Serverless service account in the serverless project." + default = {} } variable "artifact_registry_repository_name" { diff --git a/modules/secure-serverless-harness/versions.tf b/modules/secure-serverless-harness/versions.tf new file mode 100644 index 00000000..08a63016 --- /dev/null +++ b/modules/secure-serverless-harness/versions.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2022 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. + */ + +terraform { + required_version = ">= 0.13" + + required_providers { + google = { + source = "hashicorp/google" + version = "< 5.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = "< 5.0" + } + random = { + source = "hashicorp/random" + version = "< 4.0" + } + time = { + source = "hashicorp/time" + version = "< 1.0" + } + } + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-cloud-run:secure-serverless-harness/v0.6.0" + } + + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-cloud-run:secure-serverless-harness/v0.6.0" + } +} diff --git a/modules/secure-cloud-serverless-net/README.md b/modules/secure-serverless-net/README.md similarity index 97% rename from modules/secure-cloud-serverless-net/README.md rename to modules/secure-serverless-net/README.md index 568bb679..65bed947 100644 --- a/modules/secure-cloud-serverless-net/README.md +++ b/modules/secure-serverless-net/README.md @@ -50,6 +50,7 @@ module "cloud_run_network" { | resource\_names\_suffix | A suffix to concat in the end of the resources names. | `string` | `null` | no | | serverless\_project\_id | The project where cloud run is going to be deployed. | `string` | n/a | yes | | serverless\_service\_identity\_email | The Service Identity email for the serverless resource (Cloud Run or Cloud Function). | `string` | n/a | yes | +| serverless\_type | The type of resource to be used. It supports only CLOUD\_RUN or CLOUD\_FUNCTION | `string` | n/a | yes | | shared\_vpc\_name | Shared VPC name which is going to be used to create Serverless Connector. | `string` | n/a | yes | | subnet\_name | Subnet name to be re-used to create Serverless Connector. | `string` | n/a | yes | | vpc\_project\_id | The project where shared vpc is. | `string` | n/a | yes | diff --git a/modules/secure-cloud-serverless-net/firewall.tf b/modules/secure-serverless-net/firewall.tf similarity index 88% rename from modules/secure-cloud-serverless-net/firewall.tf rename to modules/secure-serverless-net/firewall.tf index 3e1dc2d0..5b319c13 100644 --- a/modules/secure-cloud-serverless-net/firewall.tf +++ b/modules/secure-serverless-net/firewall.tf @@ -23,13 +23,13 @@ module "firewall_rules" { count = var.connector_on_host_project ? 0 : 1 source = "terraform-google-modules/network/google//modules/firewall-rules" - version = "~> 6.0" + version = "~> 7.0" project_id = var.vpc_project_id network_name = var.shared_vpc_name - rules = [{ - name = "serverless-to-vpc-connector${local.suffix}" + rules = concat([{ + name = "fw-serverless-to-vpc-connector${local.suffix}" description = null priority = null direction = "INGRESS" @@ -56,7 +56,7 @@ module "firewall_rules" { } }, { - name = "vpc-connector-to-serverless${local.suffix}" + name = "fw-vpc-connector-to-serverless${local.suffix}" description = null priority = null direction = "EGRESS" @@ -83,26 +83,7 @@ module "firewall_rules" { } }, { - name = "vpc-connector-to-lb${local.suffix}" - description = null - priority = null - direction = "EGRESS" - ranges = [] - source_tags = null - source_service_accounts = null - target_tags = local.tags - target_service_accounts = null - allow = [{ - protocol = "tcp" - ports = ["80"] - }] - deny = [] - log_config = { - metadata = "INCLUDE_ALL_METADATA" - } - }, - { - name = "vpc-connector-health-checks${local.suffix}" + name = "fw-vpc-connector-health-checks${local.suffix}" description = null priority = null direction = "INGRESS" @@ -121,7 +102,7 @@ module "firewall_rules" { } }, { - name = "vpc-connector-requests${local.suffix}" + name = "fw-vpc-connector-requests${local.suffix}" description = null priority = null direction = "INGRESS" @@ -146,5 +127,25 @@ module "firewall_rules" { log_config = { metadata = "INCLUDE_ALL_METADATA" } - }] + }], var.serverless_type == "CLOUD_RUN" ? [ + { + name = "fw-vpc-connector-to-lb${local.suffix}" + description = null + priority = null + direction = "EGRESS" + ranges = ["0.0.0.0/0"] + source_tags = null + source_service_accounts = null + target_tags = local.tags + target_service_accounts = null + allow = [{ + protocol = "tcp" + ports = ["80"] + }] + deny = [] + log_config = { + metadata = "INCLUDE_ALL_METADATA" + } + } + ] : []) } diff --git a/modules/secure-cloud-serverless-net/iam.tf b/modules/secure-serverless-net/iam.tf similarity index 95% rename from modules/secure-cloud-serverless-net/iam.tf rename to modules/secure-serverless-net/iam.tf index 43e787a7..ffcf70ee 100644 --- a/modules/secure-cloud-serverless-net/iam.tf +++ b/modules/secure-serverless-net/iam.tf @@ -14,6 +14,10 @@ * limitations under the License. */ +locals { + api = var.serverless_type == "CLOUD_RUN" ? "run" : "cloudfunctions" +} + data "google_project" "serverless_project_id" { project_id = var.serverless_project_id } diff --git a/modules/secure-cloud-serverless-net/network.tf b/modules/secure-serverless-net/network.tf similarity index 100% rename from modules/secure-cloud-serverless-net/network.tf rename to modules/secure-serverless-net/network.tf diff --git a/modules/secure-cloud-serverless-net/outputs.tf b/modules/secure-serverless-net/outputs.tf similarity index 100% rename from modules/secure-cloud-serverless-net/outputs.tf rename to modules/secure-serverless-net/outputs.tf diff --git a/modules/secure-cloud-serverless-net/variables.tf b/modules/secure-serverless-net/variables.tf similarity index 89% rename from modules/secure-cloud-serverless-net/variables.tf rename to modules/secure-serverless-net/variables.tf index f7c9dd35..b5023d7a 100644 --- a/modules/secure-cloud-serverless-net/variables.tf +++ b/modules/secure-serverless-net/variables.tf @@ -77,3 +77,13 @@ variable "resource_names_suffix" { type = string default = null } + +variable "serverless_type" { + description = "The type of resource to be used. It supports only CLOUD_RUN or CLOUD_FUNCTION" + type = string + + validation { + condition = contains(["CLOUD_RUN", "CLOUD_FUNCTION"], var.serverless_type) + error_message = "unsupported value for serverless_type" + } +} diff --git a/modules/secure-cloud-serverless-net/versions.tf b/modules/secure-serverless-net/versions.tf similarity index 94% rename from modules/secure-cloud-serverless-net/versions.tf rename to modules/secure-serverless-net/versions.tf index 1ae949a6..ac55c1a1 100644 --- a/modules/secure-cloud-serverless-net/versions.tf +++ b/modules/secure-serverless-net/versions.tf @@ -29,10 +29,10 @@ terraform { } provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-cloud-run:secure-cloud-run-net/v0.6.0" + module_name = "blueprints/terraform/terraform-google-cloud-run:secure-serverless-net/v0.6.0" } provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-cloud-run:secure-cloud-run-net/v0.6.0" + module_name = "blueprints/terraform/terraform-google-cloud-run:secure-serverless-net/v0.6.0" } } diff --git a/modules/service-project-factory/main.tf b/modules/service-project-factory/main.tf new file mode 100644 index 00000000..6619ec72 --- /dev/null +++ b/modules/service-project-factory/main.tf @@ -0,0 +1,96 @@ +/** + * 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. + */ + + +locals { + api = var.serverless_type == "CLOUD_RUN" ? "run" : "cloudfunctions" +} +module "serverless_project" { + source = "terraform-google-modules/project-factory/google" + version = "~> 14.2" + random_project_id = "true" + activate_apis = var.activate_apis + name = var.project_name + org_id = var.org_id + billing_account = var.billing_account + folder_id = var.folder_name + + svpc_host_project_id = var.network_project_id + grant_network_role = var.network_project_id != "" ? true : false +} + +module "service_accounts" { + source = "terraform-google-modules/service-accounts/google" + version = "~> 4.2" + project_id = module.serverless_project.project_id + prefix = "sa" + names = [replace(lower(var.serverless_type), "_", "-")] + + depends_on = [ + module.serverless_project + ] +} + +resource "google_project_iam_member" "cloud_run_sa_roles" { + for_each = toset(var.service_account_project_roles) + project = module.serverless_project.project_id + role = each.value + member = module.service_accounts.iam_email +} + +resource "google_project_service_identity" "serverless_sa" { + provider = google-beta + + project = module.serverless_project.project_id + service = "${local.api}.googleapis.com" +} + +resource "google_service_account_iam_member" "identity_service_account_user" { + service_account_id = module.service_accounts.service_account.id + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_project_service_identity.serverless_sa.email}" +} + +resource "google_project_service_identity" "cloudbuild_sa" { + provider = google-beta + + project = module.serverless_project.project_id + service = "cloudbuild.googleapis.com" +} + +resource "google_project_service_identity" "eventarc_sa" { + provider = google-beta + + project = module.serverless_project.project_id + service = "eventarc.googleapis.com" +} + +data "google_storage_project_service_account" "gcs_account" { + project = module.serverless_project.project_id +} + +resource "google_project_iam_member" "gcs_pubsub_publishing" { + count = var.serverless_type == "CLOUD_RUN" ? 0 : 1 + project = module.serverless_project.project_id + role = "roles/pubsub.publisher" + member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}" +} + +resource "google_project_iam_member" "eventarc_service_agent" { + project = module.serverless_project.project_id + role = "roles/eventarc.serviceAgent" + member = "serviceAccount:${google_project_service_identity.eventarc_sa.email}" +} diff --git a/modules/service-project-factory/outputs.tf b/modules/service-project-factory/outputs.tf new file mode 100644 index 00000000..f8730ad6 --- /dev/null +++ b/modules/service-project-factory/outputs.tf @@ -0,0 +1,45 @@ +/** + * 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. + */ + +output "project_id" { + value = module.serverless_project.project_id + description = "Project ID of the project created to deploy Cloud Serverless." +} + +output "project_number" { + value = module.serverless_project.project_number + description = "Project number of the project created to deploy Cloud Serverless." +} + +output "service_account_email" { + value = module.service_accounts.email + description = "The service account created tin the project." +} + +output "cloud_serverless_service_identity_email" { + value = google_project_service_identity.serverless_sa.email + description = "The Cloud Serverless Service Identity email." +} + +output "services_identities" { + value = { + "eventarc" = google_project_service_identity.eventarc_sa.email, + "cloudbuild" = google_project_service_identity.cloudbuild_sa.email, + "gcs" = data.google_storage_project_service_account.gcs_account.email_address, + "serverless" = google_project_service_identity.serverless_sa.email + } + description = "Services Identities for the serverless project." +} diff --git a/modules/service-project-factory/variables.tf b/modules/service-project-factory/variables.tf new file mode 100644 index 00000000..703e1d78 --- /dev/null +++ b/modules/service-project-factory/variables.tf @@ -0,0 +1,62 @@ +/** + * 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. + */ + +variable "billing_account" { + description = "The ID of the billing account to associate this project with." + type = string +} + +variable "serverless_type" { + description = "The type of resource to be used. It supports only CLOUD_RUN or CLOUD_FUNCTION" + type = string + + validation { + condition = contains(["CLOUD_RUN", "CLOUD_FUNCTION"], var.serverless_type) + error_message = "unsupported value for serverless_type" + } +} + +variable "network_project_id" { + description = "The network project_id when using Shared VPC." + type = string + default = "" +} + +variable "project_name" { + description = "The name to give the Cloud Serverless project." + type = string +} + +variable "org_id" { + description = "The organization ID." + type = string +} + +variable "activate_apis" { + description = "The APIs to enabled when creating the project." + type = list(string) +} + +variable "folder_name" { + description = "The folder name." + type = string +} + +variable "service_account_project_roles" { + type = list(string) + description = "Common roles to apply to the Cloud Run service account in the serverless project." + default = [] +} diff --git a/modules/secure-cloud-serverless-harness/versions.tf b/modules/service-project-factory/versions.tf similarity index 94% rename from modules/secure-cloud-serverless-harness/versions.tf rename to modules/service-project-factory/versions.tf index 71d5fc8c..4102a61b 100644 --- a/modules/secure-cloud-serverless-harness/versions.tf +++ b/modules/service-project-factory/versions.tf @@ -37,10 +37,10 @@ terraform { } provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-cloud-run:secure-cloud-harness/v0.6.0" + module_name = "blueprints/terraform/terraform-google-cloud-run:service-project-factory/v0.6.0" } provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-cloud-run:secure-cloud-harness/v0.6.0" + module_name = "blueprints/terraform/terraform-google-cloud-run:service-project-factory/v0.6.0" } } diff --git a/test/integration/secure_cloud_run/secure_cloud_run_test.go b/test/integration/secure_cloud_run/secure_cloud_run_test.go index 8a681b6e..94da1bb1 100644 --- a/test/integration/secure_cloud_run/secure_cloud_run_test.go +++ b/test/integration/secure_cloud_run/secure_cloud_run_test.go @@ -26,6 +26,11 @@ import ( "github.com/tidwall/gjson" ) +type Protocols struct { + Protocol string + Ports []string +} + func getResultFieldStrSlice(rs []gjson.Result, field string) []string { s := make([]string, 0) for _, r := range rs { @@ -140,6 +145,127 @@ func TestSecureCloudRun(t *testing.T) { assert.Equal(orgPolicy.allowedValues, opOrgPolicies[0].Get("listPolicy.allowedValues").String(), fmt.Sprintf("Constraint %s should have policy %s", orgPolicy.constraint, orgPolicy.allowedValues)) } } + // Firewall Rules + for _, firewall_rules := range []struct { + name string + direction string + ranges []string + targetTags []string + sourceTags []string + allow []Protocols + }{ + { + name: "fw-serverless-to-vpc-connector", + direction: "INGRESS", + ranges: []string{"107.178.230.64/26", "35.199.224.0/19"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{"665", "666"}, + }}, + }, + { + name: "fw-vpc-connector-to-serverless", + direction: "EGRESS", + ranges: []string{"107.178.230.64/26", "35.199.224.0/19"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{"665", "666"}, + }}, + }, + { + name: "fw-vpc-connector-health-checks", + direction: "INGRESS", + ranges: []string{"130.211.0.0/22", "35.191.0.0/16", "108.170.220.0/23"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{ + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + }, + }, + { + name: "fw-vpc-connector-requests", + direction: "INGRESS", + ranges: []string{}, + sourceTags: []string{"vpc-connector"}, + targetTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{}, + }, + }, + }, + { + name: "fw-vpc-connector-to-lb", + direction: "EGRESS", + ranges: []string{"0.0.0.0/0"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "tcp", + Ports: []string{"80"}, + }, + }, + }, + } { + fwRule := gcloud.Runf(t, "compute firewall-rules describe %s --project %s", firewall_rules.name, vpcProjectId) + assert.Equal(firewall_rules.name, fwRule.Get("name").String(), fmt.Sprintf("firewall rule %s should exist", firewall_rules.name)) + assert.Equal(firewall_rules.direction, fwRule.Get("direction").String(), fmt.Sprintf("firewall rule %s direction should be %s", firewall_rules.name, firewall_rules.direction)) + assert.False(fwRule.Get("disabled").Bool(), fmt.Sprintf("firewall rule %s should be ENABLED", firewall_rules.name)) + assert.True(fwRule.Get("logConfig.enable").Bool(), fmt.Sprintf("firewall rule %s should have log configuration enabled", firewall_rules.name)) + assert.Equal("INCLUDE_ALL_METADATA", fwRule.Get("logConfig.metadata").String(), fmt.Sprintf("firewall rule %s should have log configuration metadata as INCLUDE_ALL_METADATA", firewall_rules.name)) + + if firewall_rules.direction == "EGRESS" { + assert.Equal(utils.GetResultStrSlice(fwRule.Get("destinationRanges").Array()), firewall_rules.ranges, fmt.Sprintf("firewall rule %s destination ranges should be %v", firewall_rules.name, firewall_rules.ranges)) + } else { + assert.Equal(utils.GetResultStrSlice(fwRule.Get("sourceRanges").Array()), firewall_rules.ranges, fmt.Sprintf("firewall rule %s source ranges should be %v", firewall_rules.name, firewall_rules.ranges)) + } + + assert.Equal(firewall_rules.targetTags, utils.GetResultStrSlice(fwRule.Get("targetTags").Array()), fmt.Sprintf("firewall rule %s target tags should be %v", firewall_rules.name, firewall_rules.targetTags)) + assert.Equal(firewall_rules.sourceTags, utils.GetResultStrSlice(fwRule.Get("sourceTags").Array()), fmt.Sprintf("firewall rule %s source tags should be %v", firewall_rules.name, firewall_rules.sourceTags)) + + assert.Equal(len(firewall_rules.allow), len(utils.GetResultStrSlice(fwRule.Get("allowed").Array())), fmt.Sprintf("firewall rule %s should have %d allowed", firewall_rules.name, len(firewall_rules.allow))) + for _, allow := range firewall_rules.allow { + assert.Contains(getResultFieldStrSlice(fwRule.Get("allowed").Array(), "IPProtocol"), allow.Protocol, fmt.Sprintf("firewall rule %s should allow %v protocol", firewall_rules.name, allow.Protocol)) + allowed := fwRule.Get("allowed").Array() + for _, protocol := range allowed { + if protocol.Get("IPProtocol").String() == allow.Protocol { + assert.Equal(utils.GetResultStrSlice(protocol.Get("ports").Array()), allow.Ports, fmt.Sprintf("firewall rule %s should allow only %s port to the protocol %s ", firewall_rules.name, allow.Ports, allow.Protocol)) + } + } + } + } }) + secure_cloud_run.Test() } diff --git a/test/integration/secure_cloud_run_standalone/secure_cloud_run_standalone_test.go b/test/integration/secure_cloud_run_standalone/secure_cloud_run_standalone_test.go index a03e93cf..a91066fe 100644 --- a/test/integration/secure_cloud_run_standalone/secure_cloud_run_standalone_test.go +++ b/test/integration/secure_cloud_run_standalone/secure_cloud_run_standalone_test.go @@ -25,6 +25,11 @@ import ( "github.com/tidwall/gjson" ) +type Protocols struct { + Protocol string + Ports []string +} + func getPolicyID(t *testing.T, orgID string) string { gcOpts := gcloud.WithCommonArgs([]string{"--format", "value(name)"}) op := gcloud.Run(t, fmt.Sprintf("access-context-manager policies list --organization=%s ", orgID), gcOpts) @@ -332,6 +337,127 @@ func TestSecureCloudRunStandalone(t *testing.T) { opOrgPolicies := gcloud.Run(t, fmt.Sprintf("resource-manager org-policies describe %s --project=%s", orgPolicy.constraint, serverlessProjectID), orgArgs).Array() assert.Equal(orgPolicy.allowedValues, opOrgPolicies[0].Get("listPolicy.allowedValues").String(), fmt.Sprintf("Constraint %s should have policy %s", orgPolicy.constraint, orgPolicy.allowedValues)) } + // Firewall Rules + for _, firewall_rules := range []struct { + name string + direction string + ranges []string + targetTags []string + sourceTags []string + allow []Protocols + }{ + { + name: "fw-serverless-to-vpc-connector", + direction: "INGRESS", + ranges: []string{"107.178.230.64/26", "35.199.224.0/19"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{"665", "666"}, + }}, + }, + { + name: "fw-vpc-connector-to-serverless", + direction: "EGRESS", + ranges: []string{"107.178.230.64/26", "35.199.224.0/19"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{"665", "666"}, + }}, + }, + { + name: "fw-vpc-connector-health-checks", + direction: "INGRESS", + ranges: []string{"130.211.0.0/22", "35.191.0.0/16", "108.170.220.0/23"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{ + Protocols{ + Protocol: "tcp", + Ports: []string{"667"}, + }, + }, + }, + { + name: "fw-vpc-connector-requests", + direction: "INGRESS", + ranges: []string{}, + sourceTags: []string{"vpc-connector"}, + targetTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "icmp", + Ports: []string{}, + }, + Protocols{ + Protocol: "tcp", + Ports: []string{}, + }, + Protocols{ + Protocol: "udp", + Ports: []string{}, + }, + }, + }, + { + name: "fw-vpc-connector-to-lb", + direction: "EGRESS", + ranges: []string{"0.0.0.0/0"}, + targetTags: []string{"vpc-connector"}, + sourceTags: []string{}, + allow: []Protocols{Protocols{ + Protocol: "tcp", + Ports: []string{"80"}, + }, + }, + }, + } { + fwRule := gcloud.Runf(t, "compute firewall-rules describe %s --project %s", firewall_rules.name, serverlessProjectID) + assert.Equal(firewall_rules.name, fwRule.Get("name").String(), fmt.Sprintf("firewall rule %s should exist", firewall_rules.name)) + assert.Equal(firewall_rules.direction, fwRule.Get("direction").String(), fmt.Sprintf("firewall rule %s direction should be %s", firewall_rules.name, firewall_rules.direction)) + assert.False(fwRule.Get("disabled").Bool(), fmt.Sprintf("firewall rule %s should be ENABLED", firewall_rules.name)) + assert.True(fwRule.Get("logConfig.enable").Bool(), fmt.Sprintf("firewall rule %s should have log configuration enabled", firewall_rules.name)) + assert.Equal("INCLUDE_ALL_METADATA", fwRule.Get("logConfig.metadata").String(), fmt.Sprintf("firewall rule %s should have log configuration metadata as INCLUDE_ALL_METADATA", firewall_rules.name)) + + if firewall_rules.direction == "EGRESS" { + assert.Equal(utils.GetResultStrSlice(fwRule.Get("destinationRanges").Array()), firewall_rules.ranges, fmt.Sprintf("firewall rule %s destination ranges should be %v", firewall_rules.name, firewall_rules.ranges)) + } else { + assert.Equal(utils.GetResultStrSlice(fwRule.Get("sourceRanges").Array()), firewall_rules.ranges, fmt.Sprintf("firewall rule %s source ranges should be %v", firewall_rules.name, firewall_rules.ranges)) + } + + assert.Equal(firewall_rules.targetTags, utils.GetResultStrSlice(fwRule.Get("targetTags").Array()), fmt.Sprintf("firewall rule %s target tags should be %v", firewall_rules.name, firewall_rules.targetTags)) + assert.Equal(firewall_rules.sourceTags, utils.GetResultStrSlice(fwRule.Get("sourceTags").Array()), fmt.Sprintf("firewall rule %s source tags should be %v", firewall_rules.name, firewall_rules.sourceTags)) + + assert.Equal(len(firewall_rules.allow), len(utils.GetResultStrSlice(fwRule.Get("allowed").Array())), fmt.Sprintf("firewall rule %s should have %d allowed", firewall_rules.name, len(firewall_rules.allow))) + for _, allow := range firewall_rules.allow { + assert.Contains(getResultFieldStrSlice(fwRule.Get("allowed").Array(), "IPProtocol"), allow.Protocol, fmt.Sprintf("firewall rule %s should allow %v protocol", firewall_rules.name, allow.Protocol)) + allowed := fwRule.Get("allowed").Array() + for _, protocol := range allowed { + if protocol.Get("IPProtocol").String() == allow.Protocol { + assert.Equal(utils.GetResultStrSlice(protocol.Get("ports").Array()), allow.Ports, fmt.Sprintf("firewall rule %s should allow only %s port to the protocol %s ", firewall_rules.name, allow.Ports, allow.Protocol)) + } + } + } + } + }) cloudRun.Test()