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 recovery capability for Key Vault Soft Deleted Keys, Secrets and Certificates #6716

Merged
merged 6 commits into from May 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -383,8 +383,32 @@ func resourceArmKeyVaultCertificateCreate(d *schema.ResourceData, meta interface
CertificatePolicy: &policy,
Tags: tags.Expand(t),
}
if _, err := client.CreateCertificate(ctx, keyVaultBaseUrl, name, parameters); err != nil {
return err
if resp, err := client.CreateCertificate(ctx, keyVaultBaseUrl, name, parameters); err != nil {
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredCertificate, err := client.RecoverDeletedCertificate(ctx, keyVaultBaseUrl, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Secret %q with ID: %q", name, *recoveredCertificate.ID)
if certificate := recoveredCertificate.ID; certificate != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*certificate),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Secret %q recovered with ID: %q", name, *recoveredCertificate.ID)
}
} else {
return err
}
}

log.Printf("[DEBUG] Waiting for Key Vault Certificate %q in Vault %q to be provisioned", name, keyVaultBaseUrl)
Expand Down
29 changes: 27 additions & 2 deletions azurerm/internal/services/keyvault/resource_arm_key_vault_key.go
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/Azure/go-autorest/autorest/date"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
Expand Down Expand Up @@ -221,8 +222,32 @@ func resourceArmKeyVaultKeyCreate(d *schema.ResourceData, meta interface{}) erro
parameters.KeyAttributes.Expires = &expirationUnixTime
}

if _, err := client.CreateKey(ctx, keyVaultBaseUri, name, parameters); err != nil {
return fmt.Errorf("Error Creating Key: %+v", err)
if resp, err := client.CreateKey(ctx, keyVaultBaseUri, name, parameters); err != nil {
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredKey, err := client.RecoverDeletedKey(ctx, keyVaultBaseUri, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Key %q with ID: %q", name, *recoveredKey.Key.Kid)
if kid := recoveredKey.Key.Kid; kid != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*kid),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Key %q recovered with ID: %q", name, *kid)
}
} else {
return fmt.Errorf("Error Creating Key: %+v", err)
}
}

// "" indicates the latest version
Expand Down
75 changes: 63 additions & 12 deletions azurerm/internal/services/keyvault/resource_arm_key_vault_secret.go
Expand Up @@ -3,16 +3,17 @@ package keyvault
import (
"fmt"
"log"
"net/http"
"time"

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/Azure/go-autorest/autorest/date"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
Expand Down Expand Up @@ -99,17 +100,15 @@ func resourceArmKeyVaultSecretCreate(d *schema.ResourceData, meta interface{}) e
return fmt.Errorf("Error looking up Secret %q vault url from id %q: %+v", name, keyVaultId, err)
}

if features.ShouldResourcesBeImported() {
existing, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Secret %q (Key Vault %q): %s", name, keyVaultBaseUrl, err)
}
existing, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Secret %q (Key Vault %q): %s", name, keyVaultBaseUrl, err)
}
}

if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_key_vault_secret", *existing.ID)
}
if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_key_vault_secret", *existing.ID)
}

value := d.Get("value").(string)
Expand All @@ -135,15 +134,44 @@ func resourceArmKeyVaultSecretCreate(d *schema.ResourceData, meta interface{}) e
parameters.SecretAttributes.Expires = &expirationUnixTime
}

if _, err := client.SetSecret(ctx, keyVaultBaseUrl, name, parameters); err != nil {
return err
if resp, err := client.SetSecret(ctx, keyVaultBaseUrl, name, parameters); err != nil {
// In the case that the Secret already exists in a Soft Deleted / Recoverable state we check if `recover_soft_deleted_key_vaults` is set
// and attempt recovery where appropriate
if meta.(*clients.Client).Features.KeyVault.RecoverSoftDeletedKeyVaults && utils.ResponseWasConflict(resp.Response) {
recoveredSecret, err := client.RecoverDeletedSecret(ctx, keyVaultBaseUrl, name)
if err != nil {
return err
}
log.Printf("[DEBUG] Recovering Secret %q with ID: %q", name, *recoveredSecret.ID)
// We need to wait for consistency, recovered Key Vault Child items are not as readily available as newly created
if secret := recoveredSecret.ID; secret != nil {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: []string{"available"},
Refresh: keyVaultChildItemRefreshFunc(*secret),
Delay: 30 * time.Second,
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 10,
Timeout: d.Timeout(schema.TimeoutCreate),
}

if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error waiting for Key Vault Secret %q to become available: %s", name, err)
}
log.Printf("[DEBUG] Secret %q recovered with ID: %q", name, *recoveredSecret.ID)
}
} else {
// If the error response was anything else, or `recover_soft_deleted_key_vaults` is `false` just return the error
return err
}
}

// "" indicates the latest version
read, err := client.GetSecret(ctx, keyVaultBaseUrl, name, "")
if err != nil {
return err
}

if read.ID == nil {
return fmt.Errorf("Cannot read KeyVault Secret '%s' (in key vault '%s')", name, keyVaultBaseUrl)
}
Expand Down Expand Up @@ -339,3 +367,26 @@ func resourceArmKeyVaultSecretDelete(d *schema.ResourceData, meta interface{}) e
_, err = client.DeleteSecret(ctx, id.KeyVaultBaseUrl, id.Name)
return err
}

func keyVaultChildItemRefreshFunc(secretUri string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
log.Printf("[DEBUG] Checking to see if KeyVault Secret %q is available..", secretUri)

var PTransport = &http.Transport{Proxy: http.ProxyFromEnvironment}

client := &http.Client{
Transport: PTransport,
}

conn, err := client.Get(secretUri)
if err != nil {
log.Printf("[DEBUG] Didn't find KeyVault secret at %q", secretUri)
return nil, "pending", fmt.Errorf("Error checking secret at %q: %s", secretUri, err)
}

defer conn.Body.Close()

log.Printf("[DEBUG] Found KeyVault Secret %q", secretUri)
return "available", "available", nil
}
}
Expand Up @@ -118,6 +118,38 @@ func TestAccAzureRMKeyVaultCertificate_basicGenerate(t *testing.T) {
})
}

func TestAccAzureRMKeyVaultCertificate_softDeleteRecovery(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_certificate", "test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
Providers: acceptance.SupportedProviders,
CheckDestroy: testCheckAzureRMKeyVaultCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, false),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMKeyVaultCertificateExists(data.ResourceName),
resource.TestCheckResourceAttrSet(data.ResourceName, "secret_id"),
resource.TestCheckResourceAttrSet(data.ResourceName, "certificate_data"),
),
},
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, false),
Destroy: true,
},
{
Config: testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data, true),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMKeyVaultCertificateExists(data.ResourceName),
resource.TestCheckResourceAttrSet(data.ResourceName, "secret_id"),
resource.TestCheckResourceAttrSet(data.ResourceName, "certificate_data"),
),
},
},
})
}

func TestAccAzureRMKeyVaultCertificate_basicGenerateSans(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_key_vault_certificate", "test")

Expand Down Expand Up @@ -922,3 +954,105 @@ resource "azurerm_key_vault_certificate" "test" {
}
`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString)
}

func testAccAzureRMKeyVaultCertificate_softDeleteRecovery(data acceptance.TestData, purge bool) string {
return fmt.Sprintf(`
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = "%t"
recover_soft_deleted_key_vaults = true
}
}
}

data "azurerm_client_config" "current" {
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-kvc-%d"
location = "%s"
}

resource "azurerm_key_vault" "test" {
name = "acctestkeyvault%s"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
tenant_id = data.azurerm_client_config.current.tenant_id
soft_delete_enabled = true

sku_name = "standard"

access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id

certificate_permissions = [
"create",
"delete",
"get",
"recover",
"update",
]

key_permissions = [
"create",
]

secret_permissions = [
"set",
]

storage_permissions = [
"set",
]
}
}

resource "azurerm_key_vault_certificate" "test" {
name = "acctestcert%s"
key_vault_id = azurerm_key_vault.test.id

certificate_policy {
issuer_parameters {
name = "Self"
}

key_properties {
exportable = true
key_size = 2048
key_type = "RSA"
reuse_key = true
}

lifetime_action {
action {
action_type = "AutoRenew"
}

trigger {
days_before_expiry = 30
}
}

secret_properties {
content_type = "application/x-pkcs12"
}

x509_certificate_properties {
key_usage = [
"cRLSign",
"dataEncipherment",
"digitalSignature",
"keyAgreement",
"keyCertSign",
"keyEncipherment",
]

subject = "CN=hello-world"
validity_in_months = 12
}
}
}
`, purge, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString)
}