diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index 166afb83..f1ac7932 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -27,7 +27,6 @@ steps: args: ['/bin/bash', '-c', 'cft test run all --stage init --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - prepare @@ -47,17 +46,15 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2GCSSource --stage teardown --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - cloud-func-gcs-source-verify - id: secure-cloud-func-sql-apply name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestGCF2CloudSQL --stage apply --verbose'] + args: ['/bin/bash', '-c', './test/install_build_dependencies.sh && cft test run TestGCF2CloudSQL --stage apply --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - cloud-func-init @@ -66,7 +63,6 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2CloudSQL --stage verify --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - secure-cloud-func-sql-apply @@ -75,11 +71,35 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2CloudSQL --stage teardown --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - secure-cloud-func-sql-verify +- id: secure-cloud-func-internal-server-apply + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', './test/install_build_dependencies.sh && cft test run TestCFInternalServer --stage apply --verbose'] + env: + - 'TF_VAR_org_id=$_ORG_ID' + - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' + waitFor: + - cloud-func-init +- id: secure-cloud-func-internal-server-verify + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCFInternalServer --stage verify --verbose'] + env: + - 'TF_VAR_org_id=$_ORG_ID' + - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' + waitFor: + - secure-cloud-func-internal-server-apply +- id: secure-cloud-func-internal-server-teardown + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCFInternalServer --stage teardown --verbose'] + env: + - 'TF_VAR_org_id=$_ORG_ID' + - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' + waitFor: + - secure-cloud-func-internal-server-verify + - id: cloud-func-pubsub-trigger-apply name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestGCF2PubSubTrigger --stage apply --verbose'] @@ -95,17 +115,15 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2PubSubTrigger --stage teardown --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - cloud-func-pubsub-trigger-verify - id: secure-cloud-func-bigquery-apply name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestGCF2BigqueryTrigger --stage apply --verbose'] + args: ['/bin/bash', '-c', './test/install_build_dependencies.sh && cft test run TestGCF2BigqueryTrigger --stage apply --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - cloud-func-init @@ -114,7 +132,6 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2BigqueryTrigger --stage verify --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - secure-cloud-func-bigquery-apply @@ -123,7 +140,6 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestGCF2BigqueryTrigger --stage teardown --verbose'] env: - 'TF_VAR_org_id=$_ORG_ID' - - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' waitFor: - secure-cloud-func-bigquery-verify diff --git a/examples/secure_cloud_function_bigquery_trigger/README.md b/examples/secure_cloud_function_bigquery_trigger/README.md new file mode 100644 index 00000000..143d6bbf --- /dev/null +++ b/examples/secure_cloud_function_bigquery_trigger/README.md @@ -0,0 +1,158 @@ +# Secure Cloud Function Triggered by BigQuery + +This examples shows how trigger Secure Cloud Function (2nd Gen) by BigQuery. + +The resources and services that this example will create or enable are: + +* The **secure-serverless-harness** module will: + * Create a Security Project with the following APIS enabled: + * Cloud KMS API: `cloudkms.googleapis.com` + * Create a Cloud Function project with the following APIS enabled: + * Google VPC Access API: `vpcaccess.googleapis.com` + * Compute API: `compute.googleapis.com` + * Container Registry API: `container.googleapis.com` + * Cloud Function API: `run.googleapis.com` + * Create a Shared VPC Project with: + * A Shared Network + * A firewall rule to deny all egress traffic + * A firewall rule to allow Internal APIs traffic + * A configured Private Connect + * And the following APIs enabled: + * Google VPC Access API: `vpcaccess.googleapis.com` + * Compute API: `compute.googleapis.com` + +* The **secure-serverless-network** module will: + * Create the following Firewall rules on the **Shared VPC Project**. + * Serverless to VPC Connector + * VPC Connector to Serverless + * VPC Connector Health Checks + * Create a a sub network to VPC Connector usage purpose. + * Create a Serverless Connector on the **Shared VPC Project** or **Serverless Project**. Refer to the following comparison to choose where to create Serverless Connector: + * Advantages of creating connectors in the [Shared VPC Project](https://cloud.google.com/run/docs/configuring/connecting-shared-vpc#host-project) + * Advantages of creating connectors in the [Serverless Project](https://cloud.google.com/run/docs/configuring/connecting-shared-vpc#service-projects) + * Grant the necessary roles for the Cloud Function to be able to use the VPC Connector on the Shared VPC if creating the VPC Connector in the host project: + * Grant Network User role to the [Google API Service Agent](https://cloud.google.com/compute/docs/access/service-accounts#google_apis_service_agent) service account. + * Grant VPC Access User to the [Google Cloud Functions Service Agent](https://cloud.google.com/functions/docs/concepts/iam#access_control_for_service_accounts) when deploying VPC Access. + +* The **secure-web-proxy** module will: + * Create a sub network for Regional Managed Proxy purpose + * Create the following Firewall rule on the **Shared VPC Project**: + * Cloud Build to Secure Web Proxy + * Create a VPC peering for the Shared VPC Network with: + * A Compute Global Address + * A Service Networking Connection + * Upload a example generated self-signed certificate to Certificate Manager + * Create a Gateway Security Policy with: + * A Gateway Security Policy Rule + * A Security URL Lists resource + * Create the Secure Web Proxy/Gateway (SWP/SWG) instance + +* The **secure-cloud-serverless-security** module will: + * Create KMS Keyring and Key for [customer managed encryption keys](https://cloud.google.com/run/docs/securing/using-cmek) in the **KMS Project** to be used by Cloud Function (2nd Gen) + * Enable the following Organization Policies related to Cloud Function (2nd Gen) in the **Serverless Project**: + * Allowed ingress settings - Allow HTTP traffic from private VPC sources and through GCLB. + * Allowed VPC Connector egress settings - Force the use of VPC Access Connector for all egress traffic from the function. + * Grant the following roles if groups emails are provided: + * **Serverless Administrator** group on the Service Project: + * Cloud Run Admin: `roles/run.admin` + * Cloud Functions Admin: `roles/cloudfunctions.admin` + * Network Viewer: `roles/compute.networkViewer` + * Network User: `roles/compute.networkUser` + * **Servervless Security Administrator** group on the Security project: + * Cloud Functions Viewer: `roles/cloudfunctions.viewer` + * Cloud Frun Viewer: `roles/run.viewer` + * Cloud KMS Viewer: `roles/cloudkms.viewer` + * Artifact Registry Reader: `roles/artifactregistry.reader` + * **Cloud Function (2nd Gen) developer** group on the Security project: + * Cloud Functions Developer: `roles/cloudfunctions.developer` + * Artifact Registry Writer: `roles/artifactregistry.writer` + * Cloud KMS CryptoKey Encrypter: `roles/cloudkms.cryptoKeyEncrypter` + * **Cloud Function (2nd Gen) user** group on the Service project: + * Cloud Functions Invoker: `roles/cloudfunctions.invoker` + +* The **secure-cloud-function-core** module will: + * Create a Cloud Function (2nd Gen) + * Create the Cloud Function source bucket in the same location as the Cloud Function + * Configure the EventArc Google Channel to use Customer Encryption Key in the Cloud Function location + * **Warning:** If there is another CMEK configured for the same region, it will be overwritten + * Create a private worker pool for Cloud Build configured to not use External IP + * Grant Cloud Functions Invoker to the [EventArc Trigger Service Account](https://cloud.google.com/functions/docs/calling/eventarc#trigger-identity) + * Enable [Container Registry Automatic Scanning](https://cloud.google.com/artifact-registry/docs/analysis) + +* In addition to all the secure-cloud-function resources created, this example will also create:: + * BigQuery Dataset + * BigQuery Table + * Storage Bucket to store Cloud Function source Code + * KMS Keys to be used by: + * Pub/Sub Topic + * BigQuery Dataset and Table + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| access\_context\_manager\_policy\_id | The id of the default Access Context Manager policy. Can be obtained by running `gcloud access-context-manager policies list --organization YOUR_ORGANIZATION_ID --format="value(name)"`. This variable must be provided if `create_access_context_manager_access_policy` is set to `false` | `number` | `null` | no | +| access\_level\_members | The list of members who will be in the access level. | `list(string)` | n/a | yes | +| billing\_account | The ID of the billing account to associate this project with. | `string` | n/a | yes | +| create\_access\_context\_manager\_access\_policy | Defines if Access Context Manager will be created by Terraform. If set to `false`, you must provide `access_context_manager_policy_id`. More information about Access Context Manager creation in [this documentation](https://cloud.google.com/access-context-manager/docs/create-access-level). | `bool` | n/a | yes | +| egress\_policies | A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress\_from and egress\_to.

Example: `[{ from={ identities=[], identity_type="ID_TYPE" }, to={ resources=[], operations={ "SRV_NAME"={ OP_TYPE=[] }}}}]`

Valid Values:
`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`
`SRV_NAME` = "`*`" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)
`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions). |
list(object({
from = any
to = any
}))
| `[]` | no | +| folder\_id | The ID of a folder to host the infrastructure created in this example. | `string` | `""` | no | +| ingress\_policies | A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress\_from and ingress\_to.

Example: `[{ from={ sources={ resources=[], access_levels=[] }, identities=[], identity_type="ID_TYPE" }, to={ resources=[], operations={ "SRV_NAME"={ OP_TYPE=[] }}}}]`

Valid Values:
`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`
`SRV_NAME` = "`*`" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)
`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions). |
list(object({
from = any
to = any
}))
| `[]` | no | +| org\_id | The organization ID. | `string` | n/a | yes | +| terraform\_service\_account | The e-mail of the service account who will impersionate when creating infrastructure. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| bigquery\_kms\_key | KMS Key used in the Bigquery dataset. | +| cloud\_function\_name | The service account email created to be used by Cloud Function. | +| cloudfunction\_bucket | The Cloud Function source bucket. | +| cloudfunction\_bucket\_name | Name of the Cloud Function source bucket. | +| cloudfunction\_url | The URL on which the deployed service is available. | +| connector\_id | VPC serverless connector ID. | +| network\_project\_id | The network project id. | +| restricted\_access\_level\_name | Access level name. | +| restricted\_service\_perimeter\_name | Service Perimeter name. | +| security\_project\_id | The security project id. | +| security\_project\_number | The security project number. | +| serverless\_project\_id | The serverless project id. | +| serverless\_project\_number | The serverless project number. | +| service\_account\_email | The service account email created to be used by Cloud Function. | +| service\_vpc\_name | The Network self-link created in harness. | +| service\_vpc\_self\_link | The Network self-link created in harness. | +| service\_vpc\_subnet\_name | The sub-network name created in harness. | +| table\_id | Bigquery table name. | + + + +## Requirements + +### Software + +The following dependencies must be available: + +* [Terraform](https://www.terraform.io/downloads.html) >= 1.3 +* [Terraform Provider for GCP](https://github.com/terraform-providers/terraform-provider-google) < 5.0 +* [Google Cloud SDK CLI](https://cloud.google.com/sdk/docs/install) > 428.0.0 + +### APIs +#TODO: Fill with APIs needed on SA project + +### Service Account + +A service account with the following roles must be used to provision +the resources of this module: + +* Shared VPC Project +* Organization Level + * Access Context Manager Admin: `roles/accesscontextmanager.policyAdmin` + * Organization Policy Admin: `roles/orgpolicy.policyAdmin` +* Folder Level: + * Folder Admin: `roles/resourcemanager.folderAdmin` + * Project Creator: `roles/resourcemanager.projectCreator` + * Project Deleter: `roles/resourcemanager.projectDeleter` + * Compute Shared VPC Admin: `roles/compute.xpnAdmin` +* Billing: + * Billing User: `roles/billing.user` diff --git a/examples/secure_cloud_function_bigquery_trigger/functions/bq-to-cf/main.go b/examples/secure_cloud_function_bigquery_trigger/functions/bq-to-cf/main.go index 9fb2d805..33875f99 100644 --- a/examples/secure_cloud_function_bigquery_trigger/functions/bq-to-cf/main.go +++ b/examples/secure_cloud_function_bigquery_trigger/functions/bq-to-cf/main.go @@ -61,14 +61,14 @@ func helloPubSub(ctx context.Context, e event.Event) error { log.Printf("Error listing compute regions: %s.", err.Error()) fmt.Errorf(err.Error()) } - log.Println("Regions: %v!\n", regions) + log.Printf("Regions: %v!\n", regions) buckets, err := listBuckets() if err != nil { log.Printf("Error listing project buckets: %s.", err.Error()) fmt.Errorf(err.Error()) } - log.Println("Buckets: %v!\n", buckets) + log.Printf("Buckets: %v!\n", buckets) return nil } @@ -79,7 +79,7 @@ func helloPubSub(ctx context.Context, e event.Event) error { func listBuckets() ([]string, error) { projectID := os.Getenv("PROJECT_ID") ctx := context.Background() - log.Println("Creating Client for Storage.") + log.Printf("Creating Client for Storage.") client, err := storage.NewClient(ctx) if err != nil { return nil, fmt.Errorf("storage.NewClient: %v", err) @@ -90,7 +90,7 @@ func listBuckets() ([]string, error) { defer cancel() var buckets []string - log.Println("Getting buckets in project.") + log.Printf("Getting buckets in project.") it := client.Buckets(ctx, projectID) for { battrs, err := it.Next() @@ -108,13 +108,13 @@ func listBuckets() ([]string, error) { func listComputeRegions() ([]string, error) { ctx := context.Background() - log.Println("Creating Default Client for Compute client.") + log.Printf("Creating Default Client for Compute client.") c, err := google.DefaultClient(ctx) if err != nil { log.Fatal(err) } - log.Println("Creating service for Compute client.") + log.Printf("Creating service for Compute client.") computeService, err := compute.New(c) if err != nil { log.Fatal(err) @@ -123,7 +123,7 @@ func listComputeRegions() ([]string, error) { // Project ID for this request. project := os.Getenv("PROJECT_ID") var regions []string - log.Println("Getting compute regions.") + log.Printf("Getting compute regions.") req := computeService.Regions.List(project) if err := req.Pages(ctx, func(page *compute.RegionList) error { for _, region := range page.Items { diff --git a/examples/secure_cloud_function_bigquery_trigger/main.tf b/examples/secure_cloud_function_bigquery_trigger/main.tf index 0ae9de28..78a75359 100644 --- a/examples/secure_cloud_function_bigquery_trigger/main.tf +++ b/examples/secure_cloud_function_bigquery_trigger/main.tf @@ -21,7 +21,9 @@ locals { repository_name = "rep-secure-cloud-function" table_name = "tbl_test" kms_bigquery = "key-secure-bigquery" + subnet_ip = "10.0.0.0/28" } + resource "random_id" "random_folder_suffix" { byte_length = 2 } @@ -40,11 +42,11 @@ module "secure_harness" { region = local.region location = local.location vpc_name = "vpc-secure-cloud-function" - subnet_ip = "10.0.0.0/28" + subnet_ip = local.subnet_ip 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 + access_level_members = distinct(concat(var.access_level_members, ["serviceAccount:${var.terraform_service_account}"])) key_name = "key-secure-artifact-registry" keyring_name = "krg-secure-artifact-registry" prevent_destroy = false @@ -59,6 +61,20 @@ module "secure_harness" { "prj-secure-cloud-function" = ["roles/eventarc.eventReceiver", "roles/viewer", "roles/compute.networkViewer", "roles/run.invoker"] } + network_project_extra_apis = ["networksecurity.googleapis.com"] + + serverless_project_extra_apis = { + "prj-secure-cloud-function" = ["networksecurity.googleapis.com"] + } +} + +resource "google_project_service" "network_project_apis" { + for_each = toset(["networkservices.googleapis.com", "certificatemanager.googleapis.com"]) + project = module.secure_harness.network_project_id[0] + service = each.value + disable_on_destroy = false + + depends_on = [module.secure_harness] } data "archive_file" "cf_bigquery_source" { @@ -144,6 +160,85 @@ module "bigquery" { ] } +resource "null_resource" "generate_certificate" { + triggers = { + project_id = module.secure_harness.network_project_id[0] + region = local.region + } + + provisioner "local-exec" { + when = create + command = < +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| access\_context\_manager\_policy\_id | The id of the default Access Context Manager policy. Can be obtained by running `gcloud access-context-manager policies list --organization YOUR_ORGANIZATION_ID --format="value(name)"`. This variable must be provided if `create_access_context_manager_access_policy` is set to `false` | `number` | `null` | no | +| access\_level\_members | The list of members who will be in the access level. | `list(string)` | n/a | yes | +| billing\_account | The ID of the billing account to associate this project with. | `string` | n/a | yes | +| create\_access\_context\_manager\_access\_policy | Defines if Access Context Manager will be created by Terraform. If set to `false`, you must provide `access_context_manager_policy_id`. More information about Access Context Manager creation in [this documentation](https://cloud.google.com/access-context-manager/docs/create-access-level). | `bool` | n/a | yes | +| egress\_policies | A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress\_from and egress\_to.

Example: `[{ from={ identities=[], identity_type="ID_TYPE" }, to={ resources=[], operations={ "SRV_NAME"={ OP_TYPE=[] }}}}]`

Valid Values:
`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`
`SRV_NAME` = "`*`" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)
`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions). |
list(object({
from = any
to = any
}))
| `[]` | no | +| folder\_id | The ID of a folder to host the infrastructure created in this example. | `string` | `""` | no | +| ingress\_policies | A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress\_from and ingress\_to.

Example: `[{ from={ sources={ resources=[], access_levels=[] }, identities=[], identity_type="ID_TYPE" }, to={ resources=[], operations={ "SRV_NAME"={ OP_TYPE=[] }}}}]`

Valid Values:
`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`
`SRV_NAME` = "`*`" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)
`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions). |
list(object({
from = any
to = any
}))
| `[]` | no | +| org\_id | The organization ID. | `string` | n/a | yes | +| terraform\_service\_account | The e-mail of the service account who will impersionate when creating infrastructure. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| cloud\_function\_name | The service account email created to be used by Cloud Function. | +| cloudfunction\_bucket | The Cloud Function source bucket. | +| cloudfunction\_bucket\_name | Name of the Cloud Function source bucket. | +| cloudfunction\_url | The URL on which the deployed service is available. | +| connector\_id | VPC serverless connector ID. | +| network\_project\_id | The network project id. | +| restricted\_access\_level\_name | Access level name. | +| restricted\_service\_perimeter\_name | Service Perimeter name. | +| security\_project\_id | The security project id. | +| security\_project\_number | The security project number. | +| serverless\_project\_id | The serverless project id. | +| serverless\_project\_number | The serverless project number. | +| service\_account\_email | The service account email created to be used by Cloud Function. | +| service\_vpc\_name | The Network self-link created in harness. | +| service\_vpc\_self\_link | The Network self-link created in harness. | +| service\_vpc\_subnet\_name | The sub-network name created in harness. | + + +## Requirements + +### Software + +The following dependencies must be available: + +* [Terraform](https://www.terraform.io/downloads.html) >= 1.3 +* [Terraform Provider for GCP](https://github.com/terraform-providers/terraform-provider-google) < 5.0 + +### APIs + +The Secure Cloud Function Internal Server Example will enable the following APIs to the Serverless Project: + +* Google VPC Access API: `vpcaccess.googleapis.com` +* Compute API: `compute.googleapis.com` +* Container Registry API: `container.googleapis.com` +* Artifact Registry API: `artifactregistry.googleapis.com` +* Cloud Function API: `cloudfunctions.googleapis.com` +* Cloud Run API: `run.googleapis.com` +* Service Networking API: `servicenetworking.googleapis.com` +* Cloud KMS API: `cloudkms.googleapis.com` +* Container Scanning API: `containerscanning.googleapis.com` +* Eventarc API: `eventarc.googleapis.com` +* Eventarc Publishing API: `eventarcpublishing.googleapis.com` +* Cloud Build API: `cloudbuild.googleapis.com` + +The Secure Cloud Function with Internal Server Example will enable the following APIs to the VPC Project: + +* Google VPC Access API: `vpcaccess.googleapis.com` +* Compute API: `compute.googleapis.com` +* Service Networking API: `servicenetworking.googleapis.com` +* DNS API: `dns.googleapis.com` + +The Secure Cloud Function with Internal Server Example will enable the following APIs to the Security Project: + +* Cloud KMS API: `cloudkms.googleapis.com` +* Artifact Registry API: `artifactregistry.googleapis.com` + +### Service Account + +A service account with the following roles must be used to provision the resources of this module: + +* Organization Level + * Access Context Manager Admin: `roles/accesscontextmanager.policyAdmin` + * Organization Policy Admin: `roles/orgpolicy.policyAdmin` +* Folder Level: + * Folder Admin: `roles/resourcemanager.folderAdmin` + * Project Creator: `roles/resourcemanager.projectCreator` + * Project Deleter: `roles/resourcemanager.projectDeleter` + * Compute Shared VPC Admin: `roles/compute.xpnAdmin` +* Billing: + * Billing User: `roles/billing.user` diff --git a/examples/secure_cloud_function_internal_server/function/go.mod b/examples/secure_cloud_function_internal_server/function/go.mod new file mode 100644 index 00000000..2d275316 --- /dev/null +++ b/examples/secure_cloud_function_internal_server/function/go.mod @@ -0,0 +1,5 @@ +module example.com/module/helloworld + +require ( + github.com/GoogleCloudPlatform/functions-framework-go v1.6.1 +) diff --git a/examples/secure_cloud_function_internal_server/function/main.go b/examples/secure_cloud_function_internal_server/function/main.go new file mode 100644 index 00000000..5284e73a --- /dev/null +++ b/examples/secure_cloud_function_internal_server/function/main.go @@ -0,0 +1,65 @@ +// 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 +// +// https://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. + +package helloworld + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + + "github.com/GoogleCloudPlatform/functions-framework-go/functions" +) + +func init() { + functions.HTTP("helloHTTP", helloHTTP) +} + +func helloHTTP(w http.ResponseWriter, r *http.Request) { + ipAddress := os.Getenv("TARGET_IP") + if ipAddress == "" { + log.Println("TARGET_IP environment variable not set") + http.Error(w, "TARGET_IP not set", http.StatusInternalServerError) + return + } + + url := fmt.Sprintf("http://%s:8000/index.html", ipAddress) + + // Send GET request to the server + response, err := http.Get(url) + if err != nil { + log.Printf("Failed to send GET request: %s\n", err) + http.Error(w, "Failed to send GET request", http.StatusInternalServerError) + return + } + defer response.Body.Close() + + // Read the response body + content, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Printf("Failed to read response body: %s\n", err) + http.Error(w, "Failed to read response body", http.StatusInternalServerError) + return + } + + // Log the content + log.Printf("Message returned from internal server: %s\n", string(content)) + + // Write the content to the response + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(content)) +} diff --git a/examples/secure_cloud_function_internal_server/internal_server.tf b/examples/secure_cloud_function_internal_server/internal_server.tf new file mode 100644 index 00000000..e9429c2b --- /dev/null +++ b/examples/secure_cloud_function_internal_server/internal_server.tf @@ -0,0 +1,107 @@ +/** + * 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. + */ + +resource "google_project_service_identity" "compute_identity_sa" { + provider = google-beta + + project = module.secure_harness.serverless_project_ids[0] + service = "compute.googleapis.com" +} + +module "compute_service_account" { + source = "terraform-google-modules/service-accounts/google" + version = "~> 3.0" + project_id = module.secure_harness.serverless_project_ids[0] + names = ["sa-compute-instance"] +} + +resource "google_project_iam_member" "service_account_roles" { + project = module.secure_harness.serverless_project_ids[0] + member = "serviceAccount:${module.compute_service_account.email}" + role = "roles/compute.instanceAdmin.v1" + + depends_on = [module.compute_service_account] +} + +data "google_project" "serverless_project_id" { + project_id = module.secure_harness.serverless_project_ids[0] +} + +resource "google_service_account_iam_member" "service_account_user" { + service_account_id = module.compute_service_account.service_account.id + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:service-${data.google_project.serverless_project_id.number}@compute-system.iam.gserviceaccount.com" + + depends_on = [google_project_iam_member.service_account_roles] +} + +resource "google_compute_instance" "internal_server" { + name = local.webserver_instance + project = module.secure_harness.serverless_project_ids[0] + zone = local.zone + machine_type = "e2-small" + can_ip_forward = true + + boot_disk { + initialize_params { + image = "debian-cloud/debian-11" + } + } + tags = ["https-server"] + metadata_startup_script = file("${abspath(path.module)}/web_server/internal_server_setup.sh") + + network_interface { + subnetwork = module.secure_harness.service_subnet[0] + network_ip = local.network_ip + subnetwork_project = module.secure_harness.network_project_id[0] + } + + service_account { + email = module.compute_service_account.email + scopes = ["cloud-platform"] + } + + depends_on = [ + google_service_account_iam_member.service_account_user, + module.secure_harness + ] +} + +module "internal_server_firewall_rule" { + source = "terraform-google-modules/network/google//modules/firewall-rules" + version = "~> 7.0" + project_id = module.secure_harness.network_project_id[0] + network_name = module.secure_harness.service_vpc[0].network.name + + rules = [{ + name = "fw-e-shared-restricted-internal-server" + description = "Allow Cloud Function to connect in Internal Server using the private IP" + direction = "EGRESS" + priority = 100 + + log_config = { + metadata = "INCLUDE_ALL_METADATA" + } + deny = [] + allow = [{ + protocol = "tcp" + ports = ["8000"] + }] + + ranges = ["10.0.0.0/28"] + target_tags = ["allow-google-apis", "vpc-connector"] + }] +} diff --git a/examples/secure_cloud_function_internal_server/main.tf b/examples/secure_cloud_function_internal_server/main.tf new file mode 100644 index 00000000..95c415e1 --- /dev/null +++ b/examples/secure_cloud_function_internal_server/main.tf @@ -0,0 +1,236 @@ +# /** +# * 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 { + location = "us-west1" + region = "us-west1" + zone = "us-west1-b" + repository_name = "rep-secure-cloud-function" + network_ip = "10.0.0.3" + webserver_instance = "webserver" + subnet_ip = "10.0.0.0/28" +} +resource "random_id" "random_folder_suffix" { + byte_length = 2 +} + +module "secure_harness" { + source = "GoogleCloudPlatform/cloud-run/google//modules/secure-serverless-harness" + version = "~> 0.8" + + billing_account = var.billing_account + security_project_name = "prj-security" + network_project_name = "prj-restricted-shared" + serverless_project_names = ["prj-secure-cloud-function"] + org_id = var.org_id + parent_folder_id = var.folder_id + serverless_folder_suffix = random_id.random_folder_suffix.hex + region = local.region + location = local.location + vpc_name = "vpc-secure-cloud-function" + subnet_ip = local.subnet_ip + 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 = distinct(concat(var.access_level_members, ["serviceAccount:${var.terraform_service_account}"])) + 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_FUNCTION" + use_shared_vpc = true + time_to_wait_vpc_sc_propagation = "660s" + + service_account_project_roles = { + "prj-secure-cloud-function" = [ + "roles/eventarc.eventReceiver", + "roles/viewer", + "roles/compute.networkViewer", + "roles/run.invoker" + ] + } + + network_project_extra_apis = [ + "networksecurity.googleapis.com" + ] + + serverless_project_extra_apis = { + "prj-secure-cloud-function" = [ + "networksecurity.googleapis.com" + ] + } +} + +resource "google_project_service" "network_project_apis" { + for_each = toset(["networkservices.googleapis.com", "certificatemanager.googleapis.com"]) + project = module.secure_harness.network_project_id[0] + service = each.value + disable_on_destroy = false + + depends_on = [module.secure_harness] +} + +data "archive_file" "cf-internal-server-source" { + type = "zip" + source_dir = "${path.module}/function" + output_path = "function/cloudfunction-${random_id.random_folder_suffix.hex}.zip" +} + +resource "google_storage_bucket_object" "function-source" { + source = data.archive_file.cf-internal-server-source.output_path + content_type = "application/zip" + + # Append to the MD5 checksum of the files's content + # to force the zip to be updated as soon as a change occurs + name = "src-${data.archive_file.cf-internal-server-source.output_md5}.zip" + bucket = module.secure_harness.cloudfunction_source_bucket[module.secure_harness.serverless_project_ids[0]].name + + depends_on = [ + data.archive_file.cf-internal-server-source + ] +} + +resource "null_resource" "generate_certificate" { + triggers = { + project_id = module.secure_harness.network_project_id[0] + region = local.region + } + + provisioner "local-exec" { + when = create + command = <object({
project_id = optional(string)
repo_name = string
branch_name = string
dir = optional(string)
tag_name = optional(string)
commit_sha = optional(string)
invert_regex = optional(bool, false)
}) | `null` | no | diff --git a/modules/secure-cloud-function-core/main.tf b/modules/secure-cloud-function-core/main.tf index 695ba38f..43151b4b 100644 --- a/modules/secure-cloud-function-core/main.tf +++ b/modules/secure-cloud-function-core/main.tf @@ -84,6 +84,10 @@ resource "google_cloudbuild_worker_pool" "pool" { machine_type = "e2-standard-8" no_external_ip = true } + network_config { + peered_network = var.network_id + } + } module "cloud_function" { @@ -102,13 +106,12 @@ module "cloud_function" { storage_source = var.storage_source service_config = var.service_config docker_repository = google_artifact_registry_repository.cloudfunction_repo.id - - ## THIS SHOULD BE UNCOMMENTED WHEN SECURE WEB PROXY IS READY, TO ALLOW THE PRIVATE POOL USAGE. - # worker_pool = google_cloudbuild_worker_pool.pool.id + worker_pool = google_cloudbuild_worker_pool.pool.id depends_on = [ module.cloudfunction_bucket, google_eventarc_google_channel_config.primary, + google_cloudbuild_worker_pool.pool, google_project_service.container_scanning_api, module.pubsub ] diff --git a/modules/secure-cloud-function-core/variables.tf b/modules/secure-cloud-function-core/variables.tf index 983b075e..382bc321 100644 --- a/modules/secure-cloud-function-core/variables.tf +++ b/modules/secure-cloud-function-core/variables.tf @@ -14,12 +14,16 @@ * limitations under the License. */ - variable "project_id" { description = "The project ID to deploy to." type = string } +variable "network_id" { + description = "VPC network ID which is going to be used to connect the WorkerPool." + type = string +} + variable "project_number" { description = "The project number to deploy to." type = number diff --git a/modules/secure-cloud-function/README.md b/modules/secure-cloud-function/README.md index 02a2c4cf..66fa0154 100644 --- a/modules/secure-cloud-function/README.md +++ b/modules/secure-cloud-function/README.md @@ -117,6 +117,7 @@ module "secure_cloud_run" { | location | The location where resources are going to be deployed. | `string` | n/a | yes | | max\_scale\_instances | Sets the maximum number of container instances needed to handle all incoming requests or events from each revison from Cloud Run. For more information, access this [documentation](https://cloud.google.com/run/docs/about-instance-autoscaling). | `number` | `2` | no | | min\_scale\_instances | Sets the minimum number of container instances needed to handle all incoming requests or events from each revison from Cloud Run. For more information, access this [documentation](https://cloud.google.com/run/docs/about-instance-autoscaling). | `number` | `1` | no | +| network\_id | VPC network ID which is going to be used to connect the WorkerPool. | `string` | n/a | yes | | organization\_id | The organization ID to apply the policy to. | `string` | `""` | no | | policy\_for | Policy Root: set one of the following values to determine where the policy is applied. Possible values: ["project", "folder", "organization"]. | `string` | `"project"` | no | | prevent\_destroy | Set the `prevent_destroy` lifecycle attribute on the Cloud KMS key. | `bool` | `true` | no | diff --git a/modules/secure-cloud-function/main.tf b/modules/secure-cloud-function/main.tf index 4a0ef6b4..504bd0a7 100644 --- a/modules/secure-cloud-function/main.tf +++ b/modules/secure-cloud-function/main.tf @@ -17,7 +17,7 @@ module "cloud_serverless_network" { source = "GoogleCloudPlatform/cloud-run/google//modules/secure-serverless-net" - version = "~> 0.7" + version = "~> 0.8" connector_name = var.connector_name subnet_name = var.subnet_name @@ -130,6 +130,7 @@ module "cloud_function_core" { encryption_key = module.cloud_function_security.key_self_link bucket_lifecycle_rules = var.bucket_lifecycle_rules bucket_cors = var.bucket_cors + network_id = var.network_id service_config = { max_instance_count = var.max_scale_instances diff --git a/modules/secure-cloud-function/variables.tf b/modules/secure-cloud-function/variables.tf index f945e8ac..f3a55163 100644 --- a/modules/secure-cloud-function/variables.tf +++ b/modules/secure-cloud-function/variables.tf @@ -35,6 +35,11 @@ variable "vpc_project_id" { type = string } +variable "network_id" { + description = "VPC network ID which is going to be used to connect the WorkerPool." + type = string +} + variable "key_name" { description = "The name of KMS Key to be created and used in Cloud Run." type = string diff --git a/test/install_build_dependencies.sh b/test/install_build_dependencies.sh new file mode 100755 index 00000000..7c01cb88 --- /dev/null +++ b/test/install_build_dependencies.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# 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. + +# OpenSSL is needed to generate a self-signed certificate +apk add --no-cache openssl diff --git a/test/integration/secure_cloud_function_bigquery_trigger/cloud_function2_bigquery_trigger_test.go b/test/integration/secure_cloud_function_bigquery_trigger/cloud_function2_bigquery_trigger_test.go index 83d124f4..924762ba 100644 --- a/test/integration/secure_cloud_function_bigquery_trigger/cloud_function2_bigquery_trigger_test.go +++ b/test/integration/secure_cloud_function_bigquery_trigger/cloud_function2_bigquery_trigger_test.go @@ -57,9 +57,20 @@ func GetOrgACMPolicyID(t testing.TB, orgID string) string { func TestGCF2BigqueryTrigger(t *testing.T) { orgID := utils.ValFromEnv(t, "TF_VAR_org_id") policyID := GetOrgACMPolicyID(t, orgID) + createACM := false + vars := map[string]interface{}{ - "access_context_manager_policy_id": policyID, + "create_access_context_manager_access_policy": createACM, + "access_context_manager_policy_id": policyID, + } + + if policyID == "" { + createACM = true + vars = map[string]interface{}{ + "create_access_context_manager_access_policy": createACM, + } } + restrictedServices := []string{ "accessapproval.googleapis.com", "adsdatahub.googleapis.com", @@ -222,6 +233,13 @@ func TestGCF2BigqueryTrigger(t *testing.T) { assert.Equal(subnetName, subnet.Get("name").String(), fmt.Sprintf("subnet %s should exist", subnetName)) assert.Equal(subNetRange, subnet.Get("ipCidrRange").String(), fmt.Sprintf("IP CIDR range %s should be", subNetRange)) + // Sub-network Proxy test + subnetProxyName := fmt.Sprintf("sb-swp-%s", location) + subnetProxyRange := "10.129.0.0/23" + subnetProxy := gcloud.Runf(t, "compute networks subnets describe %s --region %s --project %s", subnetProxyName, location, networkProjectID) + assert.Equal(subnetProxyName, subnetProxy.Get("name").String(), fmt.Sprintf("Subnet %s should exist", subnetProxyName)) + assert.Equal(subnetProxyRange, subnetProxy.Get("ipCidrRange").String(), fmt.Sprintf("IP CIDR range %s should be", subnetProxyRange)) + // Firewall - Deny all egress test denyAllEgressName := "fw-e-shared-restricted-65535-e-d-all-all-all" denyAllEgressRule := gcloud.Runf(t, "compute firewall-rules describe %s --project %s", denyAllEgressName, networkProjectID) @@ -246,6 +264,21 @@ func TestGCF2BigqueryTrigger(t *testing.T) { assert.Equal(1, len(allowApiEgressRule.Get("allowed.0.ports").Array()), fmt.Sprintf("firewall rule %s should allow only one port", allowApiEgressName)) assert.Equal("443", allowApiEgressRule.Get("allowed.0.ports.0").String(), fmt.Sprintf("firewall rule %s should allow port 443", allowApiEgressName)) + // Firewall - Allow egress to Secure Web Proxy + allowSwpEgressName := "fw-allow-tcp-443-egress-to-secure-web-proxy" + swpRanges := []string{subnetProxyRange, subNetRange} + allowSwpEgressRule := gcloud.Runf(t, "compute firewall-rules describe %s --project %s", allowSwpEgressName, networkProjectID) + assert.Equal(allowSwpEgressName, allowSwpEgressRule.Get("name").String(), fmt.Sprintf("firewall rule %s should exist", allowSwpEgressName)) + assert.Equal("EGRESS", allowSwpEgressRule.Get("direction").String(), fmt.Sprintf("firewall rule %s direction should be EGRESS", allowSwpEgressName)) + assert.True(allowSwpEgressRule.Get("logConfig.enable").Bool(), fmt.Sprintf("firewall rule %s should have log configuration enabled", allowSwpEgressName)) + assert.Equal(1, len(allowSwpEgressRule.Get("allowed").Array()), fmt.Sprintf("firewall rule %s should have only one allowed", allowSwpEgressName)) + assert.Equal(2, len(allowSwpEgressRule.Get("allowed.0").Map()), fmt.Sprintf("firewall rule %s should have only one protocol and ports", allowSwpEgressName)) + assert.Equal("tcp", allowSwpEgressRule.Get("allowed.0.IPProtocol").String(), fmt.Sprintf("firewall rule %s should allow tcp protocol", allowSwpEgressName)) + assert.Equal(1, len(allowSwpEgressRule.Get("allowed.0.ports").Array()), fmt.Sprintf("firewall rule %s should allow only one port", allowSwpEgressName)) + assert.Equal("443", allowSwpEgressRule.Get("allowed.0.ports.0").String(), fmt.Sprintf("firewall rule %s should allow port 443", allowSwpEgressName)) + firewallDestinationRanges := utils.GetResultStrSlice(allowSwpEgressRule.Get("destinationRanges").Array()) + assert.Subset(swpRanges, firewallDestinationRanges, fmt.Sprintf("firewall rule %s destination ranges should be %v", allowSwpEgressName, swpRanges)) + // VPC test connectorName := "con-secure-cloud-function" expectedSubnet := fmt.Sprintf("sb-restricted-%s", location) @@ -335,6 +368,60 @@ func TestGCF2BigqueryTrigger(t *testing.T) { assert.Equal(location, opDataset.Get("location").String(), fmt.Sprintf("Should have same location: %s", location)) assert.Equal(bqKmsKey, opDataset.Get("encryptionConfiguration.kmsKeyName").String(), fmt.Sprintf("Should have the KMS Key: %s", bqKmsKey)) + // Global Address test + // Networking Connection Peering test + opNetworkPeering := gcloud.Runf(t, "compute networks peerings list --network=%s --project=%s", networkName, networkProjectID).Array() + assert.Equal(1, len(opNetworkPeering), "Should have only one Network Peering.") + + // Gateway Security Policy test + opSwpPolicy := gcloud.Runf(t, "network-security gateway-security-policies list --location=%s --project=%s", location, networkProjectID).Array() + assert.Equal(1, len(opSwpPolicy), "Should have only one Gateway Security Policy") + + // URL lists test + swpUrlListValues := []string{ + "*google.com/go*", + "*github.com/GoogleCloudPlatform*", + "*github.com/cloudevents*", + "*golang.org/x*", + "*google.golang.org/*", + "*github.com/golang/*", + "*github.com/google/*", + "*github.com/googleapis/*", + "*github.com/json-iterator/go", + "*github.com/modern-go/concurrent", + "*github.com/modern-go/reflect2", + "*go.opencensus.io", + "*go.uber.org/atomic", + "*go.uber.org/multierr", + "*go.uber.org/zap", + } + opSwpUrlList := gcloud.Runf(t, "network-security url-lists list --location=%s --project=%s", location, networkProjectID).Array() + assert.Equal(1, len(opSwpUrlList), "Should have only one URL Lists") + urlLists := utils.GetResultStrSlice(opSwpUrlList[0].Get("values").Array()) + assert.Subset(swpUrlListValues, urlLists, fmt.Sprintf("Should have same URL Lists value: %v", swpUrlListValues)) + + // Gateway Security Policy Rule test + swpSessionMatcher := fmt.Sprintf("inUrlList(host(), 'projects/%s/locations/%s/urlLists/swp-url-lists')", networkProjectID, location) + opSwpPolicyRule := gcloud.Runf(t, "network-security gateway-security-policies rules list --gateway-security-policy swp-security-policy --location=%s --project=%s", location, networkProjectID).Array() + assert.Equal(1, len(opSwpPolicyRule), "Should have only one Gateway Security Policy Rule") + assert.Equal(swpSessionMatcher, opSwpPolicyRule[0].Get("sessionMatcher").String(), fmt.Sprintf("Should have same session matcher: %s", swpSessionMatcher)) + + // Secure Web Proxy test + swpName := fmt.Sprintf("projects/%s/locations/%s/gateways/secure-web-proxy", networkProjectID, location) + swpCertificate := fmt.Sprintf("projects/%s/locations/%s/certificates/swp-certificate", networkProjectID, location) + swpSecurityPolicy := fmt.Sprintf("projects/%s/locations/%s/gatewaySecurityPolicies/swp-security-policy", networkProjectID, location) + swpNetwork := fmt.Sprintf("projects/%s/global/networks/vpc-secure-cloud-function", networkProjectID) + swpSubnetwork := fmt.Sprintf("projects/%s/regions/%s/subnetworks/sb-restricted-%s", networkProjectID, location, location) + opSwpGateway := gcloud.Runf(t, "network-services gateways describe secure-web-proxy --location=%s --project=%s", location, networkProjectID) + assert.Equal(swpName, opSwpGateway.Get("name").String(), fmt.Sprintf("SWP name should be %s", swpName)) + assert.Equal("SECURE_WEB_GATEWAY", opSwpGateway.Get("type").String(), "SWP type should be SECURE_WEB_GATEWAY") + assert.Equal("10.0.0.10", opSwpGateway.Get("addresses").Array()[0].String(), "SWP first address should be 10.0.0.10") + assert.Equal("443", opSwpGateway.Get("ports").Array()[0].String(), "SWP ports should be 443") + assert.Equal(swpCertificate, opSwpGateway.Get("certificateUrls").Array()[0].String(), fmt.Sprintf("SWP certificate should be %s", swpCertificate)) + assert.Equal(swpSecurityPolicy, opSwpGateway.Get("gatewaySecurityPolicy").String(), fmt.Sprintf("SWP gateway security policy should be %s", swpSecurityPolicy)) + assert.Equal(swpNetwork, opSwpGateway.Get("network").String(), fmt.Sprintf("SWP network should be %s", swpNetwork)) + assert.Equal(swpSubnetwork, opSwpGateway.Get("subnetwork").String(), fmt.Sprintf("SWP subnetwork should be %s", swpSubnetwork)) + assert.Equal("samplescope", opSwpGateway.Get("scope").String(), "SWP scope should be samplescope") }) bqt.Test() } diff --git a/test/integration/secure_cloud_function_internal_server/cloud_function_internal_server_test.go b/test/integration/secure_cloud_function_internal_server/cloud_function_internal_server_test.go new file mode 100644 index 00000000..3f7fc472 --- /dev/null +++ b/test/integration/secure_cloud_function_internal_server/cloud_function_internal_server_test.go @@ -0,0 +1,117 @@ +// 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. + +package cloud_function_internal_server + +import ( + "fmt" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +type Protocols struct { + Protocol string + Ports []string +} + +func GetLastSplitElement(value string, sep string) string { + splitted := strings.Split(value, sep) + return splitted[len(splitted)-1] +} + +func GetResultFieldStrSlice(rs []gjson.Result, field string) []string { + s := make([]string, 0) + for _, r := range rs { + s = append(s, r.Get(field).String()) + } + return s +} + +// GetOrgACMPolicyID gets the Organization Access Context Manager Policy ID +func GetOrgACMPolicyID(t testing.TB, orgID string) string { + filter := fmt.Sprintf("parent:organizations/%s", orgID) + id := gcloud.Runf(t, "access-context-manager policies list --organization %s --filter %s --quiet", orgID, filter).Array() + if len(id) == 0 { + return "" + } + return GetLastSplitElement(id[0].Get("name").String(), "/") +} + +func TestCFInternalServer(t *testing.T) { + orgID := utils.ValFromEnv(t, "TF_VAR_org_id") + policyID := GetOrgACMPolicyID(t, orgID) + createACM := false + + vars := map[string]interface{}{ + "create_access_context_manager_access_policy": createACM, + "access_context_manager_policy_id": policyID, + } + + if policyID == "" { + createACM = true + vars = map[string]interface{}{ + "create_access_context_manager_access_policy": createACM, + } + } + + cft := tft.NewTFBlueprintTest(t, tft.WithVars(vars)) + + cft.DefineVerify(func(assert *assert.Assertions) { + + location := "us-west1" + networkProjectID := cft.GetStringOutput("network_project_id") + projectID := cft.GetStringOutput("serverless_project_id") + functionName := cft.GetStringOutput("cloud_function_name") + connectorID := cft.GetStringOutput("connector_id") + saEmail := cft.GetStringOutput("service_account_email") + + cf := gcloud.Runf(t, "functions describe %s --project %s --gen2 --region %s", functionName, projectID, location) + cfTrigger := cf.Get("eventTrigger.trigger") + assert.Equal("ACTIVE", cf.Get("state").String(), "Should be ACTIVE. Cloud Function is not successfully deployed.") + assert.Equal(connectorID, cf.Get("serviceConfig.vpcConnector").String(), fmt.Sprintf("VPC Connector should be %s. Connector was not set.", connectorID)) + assert.Equal("ALL_TRAFFIC", cf.Get("serviceConfig.vpcConnectorEgressSettings").String(), "Egress setting should be ALL_TRAFFIC.") + assert.Equal("ALLOW_INTERNAL_AND_GCLB", cf.Get("serviceConfig.ingressSettings").String(), "Ingress setting should be ALLOW_INTERNAL_AND_GCLB.") + assert.Equal(saEmail, cf.Get("serviceConfig.serviceAccountEmail").String(), fmt.Sprintf("Cloud Function should use the service account %s.", saEmail)) + assert.Equal("google.cloud.storage.object.v1.finalized", cf.Get("eventTrigger.eventType").String(), "Cloud Function EventType should be google.cloud.storage.object.v1.finalized.") + assert.NotNil(t, cfTrigger, "Trigger should exist.") + + gcloudArgsBucket := gcloud.WithCommonArgs([]string{"--project", projectID, "--json"}) + bucketName := cft.GetStringOutput("cloudfunction_bucket_name") + opBucket := gcloud.Run(t, fmt.Sprintf("alpha storage ls --buckets gs://%s", bucketName), gcloudArgsBucket).Array() + assert.Equal(bucketName, opBucket[0].Get("metadata.name").String(), fmt.Sprintf("The bucket name should be %s.", bucketName)) + assert.True(opBucket[0].Exists(), "Bucket %s should exist.", bucketName) + + instanceName := "webserver" + instanceZone := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/us-west1-b", projectID) + opInstance := gcloud.Runf(t, "compute instances describe %s --zone=us-west1-b --project=%s", instanceName, projectID) + assert.Equal(instanceName, opInstance.Get("name").String(), fmt.Sprintf("Instance name should be %s", instanceName)) + assert.Equal(instanceZone, opInstance.Get("zone").String(), fmt.Sprintf("Instance should be in zone %s", instanceZone)) + + denyAllEgressName := "fw-e-shared-restricted-internal-server" + denyAllEgressRule := gcloud.Runf(t, "compute firewall-rules describe %s --project %s", denyAllEgressName, networkProjectID) + assert.Equal(denyAllEgressName, denyAllEgressRule.Get("name").String(), fmt.Sprintf("firewall rule %s should exist", denyAllEgressName)) + assert.Equal("EGRESS", denyAllEgressRule.Get("direction").String(), fmt.Sprintf("firewall rule %s direction should be EGRESS", denyAllEgressName)) + assert.True(denyAllEgressRule.Get("logConfig.enable").Bool(), fmt.Sprintf("firewall rule %s should have log configuration enabled", denyAllEgressName)) + assert.Equal("10.0.0.0/28", denyAllEgressRule.Get("destinationRanges").Array()[0].String(), fmt.Sprintf("firewall rule %s destination ranges should be 10.0.0.0/28", denyAllEgressName)) + assert.Equal("8000", denyAllEgressRule.Get("allowed.0.ports.0").String(), fmt.Sprintf("firewall rule %s should allow port 8000", denyAllEgressName)) + + }) + cft.Test() +} diff --git a/test/integration/secure_cloud_function_with_sql/secure_cloud_function_with_sql_test.go b/test/integration/secure_cloud_function_with_sql/secure_cloud_function_with_sql_test.go index fddd3fc0..8d3e6788 100644 --- a/test/integration/secure_cloud_function_with_sql/secure_cloud_function_with_sql_test.go +++ b/test/integration/secure_cloud_function_with_sql/secure_cloud_function_with_sql_test.go @@ -43,9 +43,20 @@ func GetLastSplitElement(value string, sep string) string { func TestGCF2CloudSQL(t *testing.T) { orgID := utils.ValFromEnv(t, "TF_VAR_org_id") policyID := GetOrgACMPolicyID(t, orgID) + createACM := false + vars := map[string]interface{}{ - "access_context_manager_policy_id": policyID, + "create_access_context_manager_access_policy": createACM, + "access_context_manager_policy_id": policyID, + } + + if policyID == "" { + createACM = true + vars = map[string]interface{}{ + "create_access_context_manager_access_policy": createACM, + } } + cf2SQL := tft.NewTFBlueprintTest(t, tft.WithVars(vars)) cf2SQL.DefineVerify(func(assert *assert.Assertions) { diff --git a/test/setup/iam.tf b/test/setup/iam.tf index 750424de..492faba3 100644 --- a/test/setup/iam.tf +++ b/test/setup/iam.tf @@ -17,7 +17,8 @@ locals { int_required_roles = [ "roles/owner", - "roles/iam.serviceAccountUser" + "roles/iam.serviceAccountUser", + "roles/certificatemanager.editor" ] folder_required_roles = [ @@ -51,7 +52,7 @@ resource "google_project_iam_member" "int_test" { resource "google_folder_iam_member" "folder_test" { count = length(local.folder_required_roles) - folder = "folders/${var.folder_id}" + folder = google_folder.ci-iam-folder.id role = local.folder_required_roles[count.index] member = "serviceAccount:${google_service_account.int_test.email}" } diff --git a/test/setup/main.tf b/test/setup/main.tf index 060d0145..c7de235c 100644 --- a/test/setup/main.tf +++ b/test/setup/main.tf @@ -14,6 +14,15 @@ * limitations under the License. */ +resource "random_id" "folder-rand" { + byte_length = 2 +} + +resource "google_folder" "ci-iam-folder" { + display_name = "ci-cloud-functions-${random_id.folder-rand.hex}" + parent = "folders/${var.folder_id}" +} + module "project" { source = "terraform-google-modules/project-factory/google" version = "~> 14.0" @@ -21,7 +30,7 @@ module "project" { name = "ci-cloud-functions" random_project_id = "true" org_id = var.org_id - folder_id = var.folder_id + folder_id = google_folder.ci-iam-folder.id billing_account = var.billing_account default_service_account = "keep" @@ -41,6 +50,7 @@ module "project" { "cloudbilling.googleapis.com", "cloudkms.googleapis.com", "bigquery.googleapis.com", + "certificatemanager.googleapis.com", "sql-component.googleapis.com", "sqladmin.googleapis.com", "servicenetworking.googleapis.com" diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index ca4966b4..4d7bf9e7 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -14,6 +14,10 @@ * limitations under the License. */ +output "folder_id" { + value = google_folder.ci-iam-folder.folder_id +} + output "project_id" { value = module.project.project_id } @@ -30,7 +34,3 @@ output "terraform_service_account" { output "access_level_members" { value = ["serviceAccount:${google_service_account.int_test.email}"] } - -output "create_access_context_manager_access_policy" { - value = false -}