Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add google_kms_secret_ciphertext resource, deprecate datasource #2912

Merged
merged 7 commits into from
Jan 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/terraform
2 changes: 1 addition & 1 deletion build/terraform-beta
2 changes: 2 additions & 0 deletions products/kms/ansible.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ overrides: !ruby/object:Overrides::ResourceOverrides
Immutable purpose of CryptoKey. See
https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys#CryptoKeyPurpose
for inputs.
SecretCiphertext: !ruby/object:Overrides::Ansible::ResourceOverride
exclude: true
files: !ruby/object:Provider::Config::Files
resource:
<%= lines(indent(compile('provider/ansible/resource~compile.yaml'), 4)) -%>
32 changes: 32 additions & 0 deletions products/kms/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,35 @@ objects:
'Creating a key':
'https://cloud.google.com/kms/docs/creating-keys#create_a_key'
api: 'https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys'
- !ruby/object:Api::Resource
name: 'SecretCiphertext'
base_url: '{{crypto_key}}'
create_url: '{{crypto_key}}:encrypt'
self_link: '{{crypto_key}}'
input: true
description: |
Encrypts secret data with Google Cloud KMS and provides access to the ciphertext.
parameters:
- !ruby/object:Api::Type::String
name: 'cryptoKey'
description: |
The full name of the CryptoKey that will be used to encrypt the provided plaintext.
Format: `'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{cryptoKey}}'`
required: true
url_param_only: true
properties:
- !ruby/object:Api::Type::String
name: 'plaintext'
description: |
The plaintext to be encrypted.
required: true
- !ruby/object:Api::Type::String
name: 'ciphertext'
description: |
Contains the result of encrypting the provided plaintext, encoded in base64.
output: true
references: !ruby/object:Api::Resource::ReferenceLinks
guides:
'Encrypting and decrypting data with a symmetric key':
'https://cloud.google.com/kms/docs/encrypt-decrypt'
api: 'https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/encrypt'
34 changes: 34 additions & 0 deletions products/kms/terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,40 @@ overrides: !ruby/object:Overrides::ResourceOverrides
encoder: templates/terraform/encoders/kms_crypto_key.go.erb
update_encoder: templates/terraform/update_encoder/kms_crypto_key.go.erb
extra_schema_entry: templates/terraform/extra_schema_entry/kms_self_link.erb
SecretCiphertext: !ruby/object:Overrides::Terraform::ResourceOverride
description: |
{{description}}

~> **NOTE**: Using this resource will allow you to conceal secret data within your
resource definitions, but it does not take care of protecting that data in the
logging output, plan output, or state output. Please take care to secure your secret
data outside of resource definitions.
id_format: "{{crypto_key}}/{{ciphertext}}"
supports_indirect_user_project_override: true
exclude_import: true
exclude_validator: true
examples:
- !ruby/object:Provider::Terraform::Examples
name: "kms_secret_ciphertext_basic"
primary_resource_id: "my_password"
vars:
instance_name: "my-instance"
skip_test: true
properties:
cryptoKey: !ruby/object:Overrides::Terraform::PropertyOverride
# url_param_only already makes this ignored on read, but including
# to be extra clear that nothing gets read
ignore_read: true
danawillow marked this conversation as resolved.
Show resolved Hide resolved
plaintext: !ruby/object:Overrides::Terraform::PropertyOverride
ignore_read: true
sensitive: true
custom_expand: templates/terraform/custom_expand/base64.go.erb
ciphertext: !ruby/object:Overrides::Terraform::PropertyOverride
ignore_read: true
custom_code: !ruby/object:Provider::Terraform::CustomCode
custom_delete: templates/terraform/custom_delete/skip_delete.go.erb
post_create: templates/terraform/post_create/kms_secret_ciphertext.go.erb
decoder: templates/terraform/decoders/noop.go.erb
# This is for copying files over
files: !ruby/object:Provider::Config::Files
# These files have templating (ERB) code that will be run.
Expand Down
21 changes: 21 additions & 0 deletions templates/terraform/custom_expand/base64.go.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<%- # the license inside this block applies to this file
# Copyright 2020 Google Inc.
# 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.
-%>
func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
if v == nil {
return nil, nil
}

return base64.StdEncoding.EncodeToString([]byte(v.(string))), nil
}
1 change: 1 addition & 0 deletions templates/terraform/decoders/noop.go.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return res, nil
42 changes: 42 additions & 0 deletions templates/terraform/examples/kms_secret_ciphertext_basic.tf.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
resource "google_kms_key_ring" "keyring" {
name = "keyring-example"
location = "global"
}

resource "google_kms_crypto_key" "cryptokey" {
name = "crypto-key-example"
key_ring = google_kms_key_ring.keyring.id
rotation_period = "100000s"

lifecycle {
prevent_destroy = true
}
}

resource "google_kms_secret_ciphertext" "<%= ctx[:primary_resource_id] %>" {
crypto_key = google_kms_crypto_key.cryptokey.id
plaintext = "my-secret-password"
}

resource "google_compute_instance" "instance" {
name = "<%= ctx[:vars]['instance_name'] %>"
machine_type = "n1-standard-1"
zone = "us-central1-a"

boot_disk {
initialize_params {
image = "debian-cloud/debian-9"
}
}

network_interface {
network = "default"

access_config {
}
}

metadata = {
password = google_kms_secret_ciphertext.<%= ctx[:primary_resource_id] %>.ciphertext
}
}
12 changes: 12 additions & 0 deletions templates/terraform/post_create/kms_secret_ciphertext.go.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// we don't set anything on read and instead do it all in create
ciphertext, ok := res["ciphertext"]
if !ok {
return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.")
}
d.Set("ciphertext", ciphertext.(string))

id, err = replaceVars(d, config, "{{crypto_key}}/{{ciphertext}}")
if err != nil {
return fmt.Errorf("Error constructing id: %s", err)
}
d.SetId(id)
2 changes: 2 additions & 0 deletions templates/terraform/resource.html.markdown.erb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ This resource provides the following
<% end -%>
- `delete` - Default is <%= timeouts.delete_minutes -%> minutes.

<% unless object.exclude_import -%>
## Import

<%= object.name -%> can be imported using any of these accepted formats:
Expand All @@ -183,6 +184,7 @@ $ terraform import <% if object.min_version.name == 'beta' %>-provider=google-be
-> If you're importing a resource with beta features, make sure to include `-provider=google-beta`
as an argument so that Terraform uses the correct provider to import your resource.

<% end -%>
<% if object.base_url.include?("{{project}}") || object.supports_indirect_user_project_override -%>
## User Project Overrides

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import (

func dataSourceGoogleKmsSecretCiphertext() *schema.Resource {
return &schema.Resource{
Read: dataSourceGoogleKmsSecretCiphertextRead,
DeprecationMessage: "Use the google_kms_secret_ciphertext resource instead.",
Read: dataSourceGoogleKmsSecretCiphertextRead,
Schema: map[string]*schema.Schema{
"crypto_key": {
Type: schema.TypeString,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,113 +1,41 @@
package google

import (
"encoding/base64"
"fmt"
"log"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"google.golang.org/api/cloudkms/v1"
)

func TestAccKmsSecretCiphertext_basic(t *testing.T) {
func TestAccDataKmsSecretCiphertext_basic(t *testing.T) {
t.Parallel()

projectOrg := getTestOrgFromEnv(t)
projectBillingAccount := getTestBillingAccountFromEnv(t)

projectId := "terraform-" + acctest.RandString(10)
keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
kms := BootstrapKMSKey(t)

plaintext := fmt.Sprintf("secret-%s", acctest.RandString(10))

// The first test creates resources needed to encrypt plaintext and produce ciphertext
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testGoogleKmsCryptoKey_basic(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName),
Config: testGoogleKmsSecretCiphertext_datasource(kms.CryptoKey.Name, plaintext),
Check: func(s *terraform.State) error {
cryptoKeyId, err := getCryptoKeyId(s, "google_kms_crypto_key.crypto_key")
plaintext, err := testAccDecryptSecretDataWithCryptoKey(s, kms.CryptoKey.Name, "data.google_kms_secret_ciphertext.acceptance")

if err != nil {
return err
}

// The second test asserts that the data source created a ciphertext that can be decrypted to the correct plaintext
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testGoogleKmsSecretCiphertext_datasource(cryptoKeyId.terraformId(), plaintext),
Check: func(s *terraform.State) error {
plaintext, err := testAccDecryptSecretDataWithCryptoKey(s, cryptoKeyId, "data.google_kms_secret_ciphertext.acceptance")

if err != nil {
return err
}

return resource.TestCheckResourceAttr("data.google_kms_secret_ciphertext.acceptance", "plaintext", plaintext)(s)
},
},
},
})

return nil
return resource.TestCheckResourceAttr("data.google_kms_secret_ciphertext.acceptance", "plaintext", plaintext)(s)
},
},
},
})
}

func getCryptoKeyId(s *terraform.State, cryptoKeyResourceName string) (*kmsCryptoKeyId, error) {
config := testAccProvider.Meta().(*Config)
rs, ok := s.RootModule().Resources[cryptoKeyResourceName]
if !ok {
return nil, fmt.Errorf("Resource not found: %s", cryptoKeyResourceName)
}

return parseKmsCryptoKeyId(rs.Primary.Attributes["id"], config)
}

func testAccDecryptSecretDataWithCryptoKey(s *terraform.State, cryptoKeyId *kmsCryptoKeyId, secretCiphertextResourceName string) (string, error) {
config := testAccProvider.Meta().(*Config)
rs, ok := s.RootModule().Resources[secretCiphertextResourceName]
if !ok {
return "", fmt.Errorf("Resource not found: %s", secretCiphertextResourceName)
}
ciphertext, ok := rs.Primary.Attributes["ciphertext"]
if !ok {
return "", fmt.Errorf("Attribute 'ciphertext' not found in resource '%s'", secretCiphertextResourceName)
}

kmsDecryptRequest := &cloudkms.DecryptRequest{
Ciphertext: ciphertext,
}

decryptResponse, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Decrypt(cryptoKeyId.cryptoKeyId(), kmsDecryptRequest).Do()

if err != nil {
return "", fmt.Errorf("Error decrypting ciphertext: %s", err)
}

plaintextBytes, err := base64.StdEncoding.DecodeString(decryptResponse.Plaintext)

if err != nil {
return "", err
}

plaintext := string(plaintextBytes)
log.Printf("[INFO] Successfully decrypted ciphertext and got plaintext: %s", plaintext)

return plaintext, nil
}

func testGoogleKmsSecretCiphertext_datasource(cryptoKeyTerraformId, plaintext string) string {
return fmt.Sprintf(`
data "google_kms_secret_ciphertext" "acceptance" {
Expand Down
82 changes: 82 additions & 0 deletions third_party/terraform/tests/resource_kms_secret_ciphertext_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package google

import (
"encoding/base64"
"fmt"
"log"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"google.golang.org/api/cloudkms/v1"
)

func TestAccKmsSecretCiphertext_basic(t *testing.T) {
t.Parallel()

kms := BootstrapKMSKey(t)

plaintext := fmt.Sprintf("secret-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testGoogleKmsSecretCiphertext(kms.CryptoKey.Name, plaintext),
Check: func(s *terraform.State) error {
plaintext, err := testAccDecryptSecretDataWithCryptoKey(s, kms.CryptoKey.Name, "google_kms_secret_ciphertext.acceptance")

if err != nil {
return err
}

return resource.TestCheckResourceAttr("google_kms_secret_ciphertext.acceptance", "plaintext", plaintext)(s)
},
},
},
})
}

func testAccDecryptSecretDataWithCryptoKey(s *terraform.State, cryptoKeyId string, secretCiphertextResourceName string) (string, error) {
config := testAccProvider.Meta().(*Config)
rs, ok := s.RootModule().Resources[secretCiphertextResourceName]
if !ok {
return "", fmt.Errorf("Resource not found: %s", secretCiphertextResourceName)
}
ciphertext, ok := rs.Primary.Attributes["ciphertext"]
if !ok {
return "", fmt.Errorf("Attribute 'ciphertext' not found in resource '%s'", secretCiphertextResourceName)
}

kmsDecryptRequest := &cloudkms.DecryptRequest{
Ciphertext: ciphertext,
}

decryptResponse, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Decrypt(cryptoKeyId, kmsDecryptRequest).Do()

if err != nil {
return "", fmt.Errorf("Error decrypting ciphertext: %s", err)
}

plaintextBytes, err := base64.StdEncoding.DecodeString(decryptResponse.Plaintext)

if err != nil {
return "", err
}

plaintext := string(plaintextBytes)
log.Printf("[INFO] Successfully decrypted ciphertext and got plaintext: %s", plaintext)

return plaintext, nil
}

func testGoogleKmsSecretCiphertext(cryptoKeyTerraformId, plaintext string) string {
return fmt.Sprintf(`
resource "google_kms_secret_ciphertext" "acceptance" {
crypto_key = "%s"
plaintext = "%s"
}
`, cryptoKeyTerraformId, plaintext)
}
Loading