Skip to content

Commit

Permalink
Unblock EXTERNAL/EXTERNAL_VPC Cloud KMS key creation. (GoogleCloudPla…
Browse files Browse the repository at this point in the history
  • Loading branch information
tdbhacks authored and hao-nan-li committed Apr 9, 2024
1 parent d21c24d commit 013f0af
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 0 deletions.
7 changes: 7 additions & 0 deletions mmv1/products/kms/CryptoKey.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions mmv1/products/kms/CryptoKeyVersion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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.
20 changes: 20 additions & 0 deletions mmv1/templates/terraform/pre_update/kms_crypto_key_version.go.erb
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

0 comments on commit 013f0af

Please sign in to comment.