From a1e15b7437a5b41602e434668bf07434c2873336 Mon Sep 17 00:00:00 2001 From: Alessio Buraggina Date: Wed, 27 Mar 2024 16:17:29 -0400 Subject: [PATCH] Unblock EXTERNAL/EXTERNAL_VPC Cloud KMS key creation. (#9931) --- mmv1/products/kms/CryptoKey.yaml | 7 + mmv1/products/kms/CryptoKeyVersion.yaml | 17 ++ .../pre_update/kms_crypto_key_version.go.erb | 20 ++ .../kms/resource_kms_crypto_key_test.go | 223 ++++++++++++++++++ 4 files changed, 267 insertions(+) create mode 100644 mmv1/templates/terraform/pre_update/kms_crypto_key_version.go.erb diff --git a/mmv1/products/kms/CryptoKey.yaml b/mmv1/products/kms/CryptoKey.yaml index e393a61a424b..2cad986a77fb 100644 --- a/mmv1/products/kms/CryptoKey.yaml +++ b/mmv1/products/kms/CryptoKey.yaml @@ -156,3 +156,10 @@ properties: description: | Whether this key may contain imported versions only. default_from_api: true + - !ruby/object:Api::Type::String + name: 'cryptoKeyBackend' + immutable: true + description: | + The resource name of the backend environment associated with all CryptoKeyVersions within this CryptoKey. + The resource name is in the format "projects/*/locations/*/ekmConnections/*" and only applies to "EXTERNAL_VPC" keys. + default_from_api: true diff --git a/mmv1/products/kms/CryptoKeyVersion.yaml b/mmv1/products/kms/CryptoKeyVersion.yaml index 4fc87f93a051..3fbb15563de2 100644 --- a/mmv1/products/kms/CryptoKeyVersion.yaml +++ b/mmv1/products/kms/CryptoKeyVersion.yaml @@ -39,6 +39,7 @@ examples: custom_code: !ruby/object:Provider::Terraform::CustomCode custom_delete: templates/terraform/custom_delete/kms_crypto_key_version.erb custom_import: templates/terraform/custom_import/kms_crypto_key_version.go.erb + pre_update: templates/terraform/pre_update/kms_crypto_key_version.go.erb parameters: - !ruby/object:Api::Type::String name: 'cryptoKey' @@ -123,6 +124,9 @@ properties: name: 'externalProtectionLevelOptions' description: | ExternalProtectionLevelOptions stores a group of additional fields for configuring a CryptoKeyVersion that are specific to the EXTERNAL protection level and EXTERNAL_VPC protection levels. + deprecation_message: >- + `externalProtectionLevelOptions` is being un-nested from the `attestation` field. + Please use the top level `externalProtectionLevelOptions` field instead. properties: - !ruby/object:Api::Type::String name: 'externalKeyUri' @@ -132,3 +136,16 @@ properties: name: 'ekmConnectionKeyPath' description: | The path to the external key material on the EKM when using EkmConnection e.g., "v0/my/key". Set this field instead of externalKeyUri when using an EkmConnection. + - !ruby/object:Api::Type::NestedObject + name: 'externalProtectionLevelOptions' + description: | + ExternalProtectionLevelOptions stores a group of additional fields for configuring a CryptoKeyVersion that are specific to the EXTERNAL protection level and EXTERNAL_VPC protection levels. + properties: + - !ruby/object:Api::Type::String + name: 'externalKeyUri' + description: | + The URI for an external resource that this CryptoKeyVersion represents. + - !ruby/object:Api::Type::String + name: 'ekmConnectionKeyPath' + description: | + The path to the external key material on the EKM when using EkmConnection e.g., "v0/my/key". Set this field instead of externalKeyUri when using an EkmConnection. diff --git a/mmv1/templates/terraform/pre_update/kms_crypto_key_version.go.erb b/mmv1/templates/terraform/pre_update/kms_crypto_key_version.go.erb new file mode 100644 index 000000000000..7db52d15e778 --- /dev/null +++ b/mmv1/templates/terraform/pre_update/kms_crypto_key_version.go.erb @@ -0,0 +1,20 @@ +// The generated code does not support conditional update masks. +newUpdateMask := []string{} +if d.HasChange("state") { + newUpdateMask = append(newUpdateMask, "state") +} + +// Validate updated fields based on protection level (EXTERNAL vs EXTERNAL_VPC) +if d.HasChange("external_protection_level_options") { + if d.Get("protection_level") == "EXTERNAL" { + newUpdateMask = append(newUpdateMask, "externalProtectionLevelOptions.externalKeyUri") + } else if d.Get("protection_level") == "EXTERNAL_VPC" { + newUpdateMask = append(newUpdateMask, "externalProtectionLevelOptions.ekmConnectionKeyPath") + } +} +// updateMask is a URL parameter but not present in the schema, so ReplaceVars +// won't set it +url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(newUpdateMask, ",")}) +if err != nil { + return err +} diff --git a/mmv1/third_party/terraform/services/kms/resource_kms_crypto_key_test.go b/mmv1/third_party/terraform/services/kms/resource_kms_crypto_key_test.go index 29c650fa0ecb..558aee66e084 100644 --- a/mmv1/third_party/terraform/services/kms/resource_kms_crypto_key_test.go +++ b/mmv1/third_party/terraform/services/kms/resource_kms_crypto_key_test.go @@ -538,6 +538,85 @@ func TestAccKmsCryptoKeyVersion_patch(t *testing.T) { }) } +func TestAccKmsCryptoKeyVersion_externalProtectionLevelOptions(t *testing.T) { + t.Parallel() + + projectId := fmt.Sprintf("tf-test-%d", acctest.RandInt(t)) + projectOrg := envvar.GetTestOrgFromEnv(t) + projectBillingAccount := envvar.GetTestBillingAccountFromEnv(t) + keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + keyUri := "data.google_secret_manager_secret_version.key_uri.secret_data" + updatedKeyUri := "data.google_secret_manager_secret_version.key_uri_updated.secret_data" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri), + }, + { + ResourceName: "google_kms_crypto_key_version.crypto_key_version", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + { + Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, updatedKeyUri), + }, + { + ResourceName: "google_kms_crypto_key_version.crypto_key_version", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + }, + }) +} + +func TestAccKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(t *testing.T) { + // This test relies on manual steps to set up the EkmConnection used for the + // CryptoKeyVersion creation, which means we can't spin up a temporary project. + // We also can't use bootstrapped keys because that would defeat the purpose of + // this key creation test, so we skip this test for VCR to avoid KMS resource + // accumulation in the TF test project (since KMS resources can't be deleted). + acctest.SkipIfVcr(t) + t.Parallel() + + projectId := envvar.GetTestProjectFromEnv() + keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + ekmConnectionName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + keyPath := "data.google_secret_manager_secret_version.key_path.secret_data" + updatedKeyPath := "data.google_secret_manager_secret_version.key_path_updated.secret_data" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, keyPath), + }, + { + ResourceName: "google_kms_crypto_key_version.crypto_key_version", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + { + Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, updatedKeyPath), + }, + { + ResourceName: "google_kms_crypto_key_version.crypto_key_version", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + }, + }) +} + // This test runs in its own project, otherwise the test project would start to get filled // with undeletable resources func testGoogleKmsCryptoKey_basic(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName string) string { @@ -953,3 +1032,147 @@ resource "google_kms_crypto_key_version" "crypto_key_version" { } `, projectId, projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, preventDestroy, state) } + +func testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + name = "%s" + project_id = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_service" "acceptance" { + project = google_project.acceptance.project_id + service = "cloudkms.googleapis.com" +} + +resource "google_kms_key_ring" "key_ring" { + project = google_project_service.acceptance.project + name = "%s" + location = "us-central1" +} + +resource "google_kms_crypto_key" "crypto_key" { + name = "%s" + key_ring = google_kms_key_ring.key_ring.id + + version_template { + algorithm = "EXTERNAL_SYMMETRIC_ENCRYPTION" + protection_level = "EXTERNAL" + } + + labels = { + key = "value" + } + skip_initial_version_creation = true +} + +data "google_secret_manager_secret_version" "key_uri" { + secret = "external-full-key-uri" + project = "315636579862" +} +data "google_secret_manager_secret_version" "key_uri_updated" { + secret = "external-full-key-uri-update-test" + project = "315636579862" +} + +resource "google_kms_crypto_key_version" "crypto_key_version" { + crypto_key = google_kms_crypto_key.crypto_key.id + external_protection_level_options { + external_key_uri = %s + } +} +`, projectId, projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri) +} + +// EkmConnection setup and creation is based off of resource_kms_ekm_connection_test.go +func testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, keyPath string) string { + return fmt.Sprintf(` +data "google_project" "vpc-project" { + project_id = "cloud-ekm-refekm-playground" +} +data "google_project" "project" { + project_id = "%s" +} + +data "google_secret_manager_secret_version" "raw_der" { + secret = "playground-cert" + project = "315636579862" +} +data "google_secret_manager_secret_version" "hostname" { + secret = "external-uri" + project = "315636579862" +} +data "google_secret_manager_secret_version" "servicedirectoryservice" { + secret = "external-servicedirectoryservice" + project = "315636579862" +} + +resource "google_project_iam_member" "add_sdviewer" { + project = data.google_project.vpc-project.number + role = "roles/servicedirectory.viewer" + member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-ekms.iam.gserviceaccount.com" +} +resource "google_project_iam_member" "add_pscAuthorizedService" { + project = data.google_project.vpc-project.number + role = "roles/servicedirectory.pscAuthorizedService" + member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-ekms.iam.gserviceaccount.com" +} + +resource "google_kms_ekm_connection" "example-ekmconnection" { + name = "%s" + location = "us-central1" + key_management_mode = "MANUAL" + service_resolvers { + service_directory_service = data.google_secret_manager_secret_version.servicedirectoryservice.secret_data + hostname = data.google_secret_manager_secret_version.hostname.secret_data + server_certificates { + raw_der = data.google_secret_manager_secret_version.raw_der.secret_data + } + } + depends_on = [ + google_project_iam_member.add_pscAuthorizedService, + google_project_iam_member.add_sdviewer + ] +} + +resource "google_kms_key_ring" "key_ring" { + project = data.google_project.project.project_id + name = "%s" + location = "us-central1" +} + +resource "google_kms_crypto_key" "crypto_key" { + name = "%s" + key_ring = google_kms_key_ring.key_ring.id + + version_template { + algorithm = "EXTERNAL_SYMMETRIC_ENCRYPTION" + protection_level = "EXTERNAL_VPC" + } + + labels = { + key = "value" + } + crypto_key_backend = google_kms_ekm_connection.example-ekmconnection.id + skip_initial_version_creation = true +} + +data "google_secret_manager_secret_version" "key_path" { + secret = "external-keypath" + project = "315636579862" +} +data "google_secret_manager_secret_version" "key_path_updated" { + secret = "external-keypath-update-test" + project = "315636579862" +} + +resource "google_kms_crypto_key_version" "crypto_key_version" { + crypto_key = google_kms_crypto_key.crypto_key.id + external_protection_level_options { + ekm_connection_key_path = %s + } +} +`, projectId, ekmConnectionName, keyRingName, cryptoKeyName, keyPath) +}