terraform-provider-google/google/resource_kms_crypto_key.go
The Magician f306b84d31 Make sure KMS key "deletion" disables rotation (#3624)
Signed-off-by: Modular Magician <magic-modules@google.com>
2019-05-15 10:25:09 -07:00

388 lines
11 KiB
Go

package google
import (
"fmt"
"github.com/hashicorp/terraform/helper/validation"
"log"
"regexp"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/cloudkms/v1"
)
func resourceKmsCryptoKey() *schema.Resource {
return &schema.Resource{
Create: resourceKmsCryptoKeyCreate,
Read: resourceKmsCryptoKeyRead,
Update: resourceKmsCryptoKeyUpdate,
Delete: resourceKmsCryptoKeyDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"key_ring": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: kmsCryptoKeyRingsEquivalent,
},
"rotation_period": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: orEmpty(validateKmsCryptoKeyRotationPeriod),
},
"version_template": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"algorithm": {
Type: schema.TypeString,
Required: true,
},
"protection_level": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "SOFTWARE",
ValidateFunc: validation.StringInSlice([]string{"SOFTWARE", "HSM", ""}, false),
},
},
},
},
"self_link": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func kmsCryptoKeyRingsEquivalent(k, old, new string, d *schema.ResourceData) bool {
keyRingIdWithSpecifiersRegex := regexp.MustCompile("^projects/(" + ProjectRegex + ")/locations/([a-z0-9-])+/keyRings/([a-zA-Z0-9_-]{1,63})$")
normalizedKeyRingIdRegex := regexp.MustCompile("^(" + ProjectRegex + ")/([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})$")
if matches := keyRingIdWithSpecifiersRegex.FindStringSubmatch(new); matches != nil {
normMatches := normalizedKeyRingIdRegex.FindStringSubmatch(old)
return normMatches != nil && normMatches[1] == matches[1] && normMatches[2] == matches[2] && normMatches[3] == matches[3]
}
return false
}
type kmsCryptoKeyId struct {
KeyRingId kmsKeyRingId
Name string
}
func (s *kmsCryptoKeyId) cryptoKeyId() string {
return fmt.Sprintf("%s/cryptoKeys/%s", s.KeyRingId.keyRingId(), s.Name)
}
func (s *kmsCryptoKeyId) terraformId() string {
return fmt.Sprintf("%s/%s", s.KeyRingId.terraformId(), s.Name)
}
func resourceKmsCryptoKeyCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
keyRingId, err := parseKmsKeyRingId(d.Get("key_ring").(string), config)
if err != nil {
return err
}
cryptoKeyId := &kmsCryptoKeyId{
KeyRingId: *keyRingId,
Name: d.Get("name").(string),
}
key := cloudkms.CryptoKey{
Purpose: "ENCRYPT_DECRYPT",
VersionTemplate: expandVersionTemplate(d.Get("version_template").([]interface{})),
}
if d.Get("rotation_period") != "" {
rotationPeriod := d.Get("rotation_period").(string)
nextRotation, err := kmsCryptoKeyNextRotation(time.Now(), rotationPeriod)
if err != nil {
return fmt.Errorf("Error setting CryptoKey rotation period: %s", err.Error())
}
key.NextRotationTime = nextRotation
key.RotationPeriod = rotationPeriod
}
cryptoKey, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Create(cryptoKeyId.KeyRingId.keyRingId(), &key).CryptoKeyId(cryptoKeyId.Name).Do()
if err != nil {
return fmt.Errorf("Error creating CryptoKey: %s", err.Error())
}
log.Printf("[DEBUG] Created CryptoKey %s", cryptoKey.Name)
d.SetId(cryptoKeyId.cryptoKeyId())
return resourceKmsCryptoKeyRead(d, meta)
}
func resourceKmsCryptoKeyUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
cryptoKeyId, err := parseKmsCryptoKeyId(d.Id(), config)
if err != nil {
return err
}
key := cloudkms.CryptoKey{}
if d.HasChange("rotation_period") && d.Get("rotation_period") != "" {
rotationPeriod := d.Get("rotation_period").(string)
nextRotation, err := kmsCryptoKeyNextRotation(time.Now(), rotationPeriod)
if err != nil {
return fmt.Errorf("Error setting CryptoKey rotation period: %s", err.Error())
}
key.NextRotationTime = nextRotation
key.RotationPeriod = rotationPeriod
}
if d.HasChange("version_template") {
key.VersionTemplate = expandVersionTemplate(d.Get("version_template").([]interface{}))
}
cryptoKey, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Patch(cryptoKeyId.cryptoKeyId(), &key).UpdateMask("rotation_period,next_rotation_time").Do()
if err != nil {
return fmt.Errorf("Error updating CryptoKey: %s", err.Error())
}
log.Printf("[DEBUG] Updated CryptoKey %s", cryptoKey.Name)
d.SetId(cryptoKeyId.cryptoKeyId())
return resourceKmsCryptoKeyRead(d, meta)
}
func resourceKmsCryptoKeyRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
cryptoKeyId, err := parseKmsCryptoKeyId(d.Id(), config)
if err != nil {
return err
}
log.Printf("[DEBUG] Executing read for KMS CryptoKey %s", cryptoKeyId.cryptoKeyId())
cryptoKey, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Get(cryptoKeyId.cryptoKeyId()).Do()
if err != nil {
return fmt.Errorf("Error reading CryptoKey: %s", err)
}
d.Set("key_ring", cryptoKeyId.KeyRingId.terraformId())
d.Set("name", cryptoKeyId.Name)
d.Set("rotation_period", cryptoKey.RotationPeriod)
d.Set("self_link", cryptoKey.Name)
if err = d.Set("version_template", flattenVersionTemplate(cryptoKey.VersionTemplate)); err != nil {
return fmt.Errorf("Error setting version_template in state: %s", err.Error())
}
d.SetId(cryptoKeyId.cryptoKeyId())
return nil
}
func clearCryptoKeyVersions(cryptoKeyId *kmsCryptoKeyId, config *Config) error {
versionsClient := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions
versionsResponse, err := versionsClient.List(cryptoKeyId.cryptoKeyId()).Do()
if err != nil {
return err
}
for _, version := range versionsResponse.CryptoKeyVersions {
request := &cloudkms.DestroyCryptoKeyVersionRequest{}
_, err = versionsClient.Destroy(version.Name, request).Do()
if err != nil {
return err
}
}
return nil
}
func disableCryptoKeyRotation(cryptoKeyId *kmsCryptoKeyId, config *Config) error {
keyClient := config.clientKms.Projects.Locations.KeyRings.CryptoKeys
_, err := keyClient.Patch(cryptoKeyId.cryptoKeyId(), &cloudkms.CryptoKey{
NullFields: []string{"rotationPeriod", "nextRotationTime"},
}).
UpdateMask("rotationPeriod,nextRotationTime").Do()
return err
}
// Because KMS CryptoKey keys cannot be deleted (in GCP proper), we "delete"
// the key ring by
// a) marking all key versions for destruction (24hr soft-delete)
// b) disabling rotation of the key
// c) removing it from state
// This disables all usage of previous versions of the key and makes it
// generally useless for encryption and decryption of data.
// Re-creation of this resource through Terraform will produce an error.
func resourceKmsCryptoKeyDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
cryptoKeyId, err := parseKmsCryptoKeyId(d.Id(), config)
if err != nil {
return err
}
log.Printf(`
[WARNING] KMS CryptoKey resources cannot be deleted from GCP. The CryptoKey %s will be removed from Terraform state,
and all its CryptoKeyVersions will be destroyed, but it will still be present on the server.`, cryptoKeyId.cryptoKeyId())
// Delete all versions of the key
if err := clearCryptoKeyVersions(cryptoKeyId, config); err != nil {
return err
}
// Make sure automatic key rotation is disabled.
if err := disableCryptoKeyRotation(cryptoKeyId, config); err != nil {
return fmt.Errorf(
"While cryptoKeyVersions were cleared, Terraform was unable to disable automatic rotation of key due to an error: %s."+
"Please retry or manually disable automatic rotation to prevent creation of a new version of this key.", err)
}
d.SetId("")
return nil
}
func expandVersionTemplate(configured []interface{}) *cloudkms.CryptoKeyVersionTemplate {
if configured == nil || len(configured) == 0 {
return nil
}
data := configured[0].(map[string]interface{})
return &cloudkms.CryptoKeyVersionTemplate{
Algorithm: data["algorithm"].(string),
ProtectionLevel: data["protection_level"].(string),
}
}
func flattenVersionTemplate(versionTemplate *cloudkms.CryptoKeyVersionTemplate) []map[string]interface{} {
if versionTemplate == nil {
return nil
}
versionTemplateSchema := make([]map[string]interface{}, 0, 1)
data := map[string]interface{}{
"algorithm": versionTemplate.Algorithm,
"protection_level": versionTemplate.ProtectionLevel,
}
versionTemplateSchema = append(versionTemplateSchema, data)
return versionTemplateSchema
}
func validateKmsCryptoKeyRotationPeriod(value interface{}, _ string) (ws []string, errors []error) {
period := value.(string)
pattern := regexp.MustCompile("^([0-9.]*\\d)s$")
match := pattern.FindStringSubmatch(period)
if len(match) == 0 {
errors = append(errors, fmt.Errorf("Invalid rotation period format: %s", period))
// Cannot continue to validate because we cannot extract a number.
return
}
number := match[1]
seconds, err := strconv.ParseFloat(number, 64)
if err != nil {
errors = append(errors, err)
} else {
if seconds < 86400.0 {
errors = append(errors, fmt.Errorf("Rotation period must be greater than one day"))
}
parts := strings.Split(number, ".")
if len(parts) > 1 && len(parts[1]) > 9 {
errors = append(errors, fmt.Errorf("Rotation period cannot have more than 9 fractional digits"))
}
}
return
}
func kmsCryptoKeyNextRotation(now time.Time, period string) (result string, err error) {
var duration time.Duration
duration, err = time.ParseDuration(period)
if err == nil {
result = now.UTC().Add(duration).Format(time.RFC3339Nano)
}
return
}
func parseKmsCryptoKeyId(id string, config *Config) (*kmsCryptoKeyId, error) {
parts := strings.Split(id, "/")
cryptoKeyIdRegex := regexp.MustCompile("^(" + ProjectRegex + ")/([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})/([a-zA-Z0-9_-]{1,63})$")
cryptoKeyIdWithoutProjectRegex := regexp.MustCompile("^([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})/([a-zA-Z0-9_-]{1,63})$")
cryptoKeyRelativeLinkRegex := regexp.MustCompile("^projects/(" + ProjectRegex + ")/locations/([a-z0-9-]+)/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})$")
if cryptoKeyIdRegex.MatchString(id) {
return &kmsCryptoKeyId{
KeyRingId: kmsKeyRingId{
Project: parts[0],
Location: parts[1],
Name: parts[2],
},
Name: parts[3],
}, nil
}
if cryptoKeyIdWithoutProjectRegex.MatchString(id) {
if config.Project == "" {
return nil, fmt.Errorf("The default project for the provider must be set when using the `{location}/{keyRingName}/{cryptoKeyName}` id format.")
}
return &kmsCryptoKeyId{
KeyRingId: kmsKeyRingId{
Project: config.Project,
Location: parts[0],
Name: parts[1],
},
Name: parts[2],
}, nil
}
if parts := cryptoKeyRelativeLinkRegex.FindStringSubmatch(id); parts != nil {
return &kmsCryptoKeyId{
KeyRingId: kmsKeyRingId{
Project: parts[1],
Location: parts[2],
Name: parts[3],
},
Name: parts[4],
}, nil
}
return nil, fmt.Errorf("Invalid CryptoKey id format, expecting `{projectId}/{locationId}/{KeyringName}/{cryptoKeyName}` or `{locationId}/{keyRingName}/{cryptoKeyName}.`")
}