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

Include support to delete key metadata for kv-v2 generic secrets #1254

Merged
merged 12 commits into from
Dec 17, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
IMPROVEMENTS:
* `data/policy_document`: Add support for `patch` capability for vault-1.9+. ([#1238](https://github.com/hashicorp/terraform-provider-vault/pull/1238))
* `resource/database_secret_backend_connection`: Add support for InfluxDB connections ([#1121](https://github.com/hashicorp/terraform-provider-vault/pull/1121))
* `resource/generic_secret`: Add support for deleting all version data for a KV-V2 secret ([#1254](https://github.com/hashicorp/terraform-provider-vault/pull/1254))

## 3.0.1 (November 23, 2021)

Expand Down
7 changes: 4 additions & 3 deletions vault/import_generic_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ func TestAccGenericSecret_importBasic(t *testing.T) {
Check: testResourceGenericSecret_initialCheck(path),
},
{
ResourceName: "vault_generic_secret.test",
ImportState: true,
ImportStateVerify: true,
ResourceName: "vault_generic_secret.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"delete_all_versions"},
},
},
})
Expand Down
14 changes: 13 additions & 1 deletion vault/resource_generic_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ func genericSecretResource(name string) *schema.Resource {
Description: "Map of strings read from Vault.",
Sensitive: true,
},

"delete_all_versions": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Only applicable for kv-v2 stores. If set, permanently deletes all versions for the specified key.",
},
},
}
}
Expand Down Expand Up @@ -157,7 +164,12 @@ func genericSecretResourceDelete(d *schema.ResourceData, meta interface{}) error
}

if v2 {
path = addPrefixToVKVPath(path, mountPath, "data")
base := "data"
deleteAllVersions := d.Get("delete_all_versions").(bool)
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
if deleteAllVersions {
base = "metadata"
}
path = addPrefixToVKVPath(path, mountPath, base)
}

log.Printf("[DEBUG] Deleting vault_generic_secret from %q", path)
Expand Down
155 changes: 154 additions & 1 deletion vault/resource_generic_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ func TestResourceGenericSecret_deleted(t *testing.T) {
})
}

func TestResourceGenericSecret_deleteAllVersions(t *testing.T) {
path := acctest.RandomWithPrefix("secretsv2/test")
resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAllVersionDestroy,
Steps: []resource.TestStep{
{
Config: testResourceGenericSecret_initialConfig_v2(path, false),
Check: testResourceGenericSecret_initialCheck_v2(path, "zap", 1),
},
{
Config: testResourceGenericSecret_initialConfig_v2(path, true),
Check: testResourceGenericSecret_initialCheck_v2(path, "zoop", 2),
},
},
})
}

func testResourceGenericSecret_initialConfig(path string) string {
return fmt.Sprintf(`
resource "vault_mount" "v1" {
Expand All @@ -74,6 +93,46 @@ EOT
}`, path)
}

func testResourceGenericSecret_initialConfig_v2(path string, isUpdate bool) string {
result := fmt.Sprintf(`
resource "vault_mount" "v2" {
path = "secretsv2"
type = "kv"
options = {
version = "2"
}
}

`)
if !isUpdate {
result += fmt.Sprintf(`
resource "vault_generic_secret" "test" {
depends_on = ["vault_mount.v2"]
path = "%s"
delete_all_versions = true
data_json = <<EOT
{
"zip": "zap"
}
EOT
}`, path)
} else {
result += fmt.Sprintf(`
resource "vault_generic_secret" "test" {
depends_on = ["vault_mount.v2"]
path = "%s"
delete_all_versions = true
data_json = <<EOT
{
"zip": "zoop"
}
EOT
}`, path)
}

return result
}

func testResourceGenericSecret_initialCheck(expectedPath string) resource.TestCheckFunc {
return func(s *terraform.State) error {
resourceState := s.Modules[0].Resources["vault_generic_secret.test"]
Expand All @@ -96,13 +155,15 @@ func testResourceGenericSecret_initialCheck(expectedPath string) resource.TestCh
}

client := testProvider.Meta().(*api.Client)

secret, err := client.Logical().Read(path)
if err != nil {
return fmt.Errorf("error reading back secret: %s", err)
}

data := secret.Data
// Test the JSON
if got, want := secret.Data["zip"], "zap"; got != want {
if got, want := data["zip"], "zap"; got != want {
return fmt.Errorf("'zip' data is %q; want %q", got, want)
}

Expand All @@ -115,6 +176,98 @@ func testResourceGenericSecret_initialCheck(expectedPath string) resource.TestCh
}
}

func testResourceGenericSecret_initialCheck_v2(expectedPath string, wantValue string, versionCount int) resource.TestCheckFunc {
return func(s *terraform.State) error {
resourceState := s.Modules[0].Resources["vault_generic_secret.test"]
if resourceState == nil {
return fmt.Errorf("resource not found in state")
}

instanceState := resourceState.Primary
if instanceState == nil {
return fmt.Errorf("resource has no primary instance")
}

path := instanceState.ID

if path != instanceState.Attributes["path"] {
return fmt.Errorf("id doesn't match path")
}
if path != expectedPath {
return fmt.Errorf("unexpected secret path")
}

client := testProvider.Meta().(*api.Client)

// Checking KV-V2 Secrets
resp, err := client.Logical().List("secretsv2/metadata")
if err != nil {
return fmt.Errorf("unable to list secrets metadata: %s", err)
}

if resp == nil {
return fmt.Errorf("expected kv-v2 secrets, got nil")
}
keys := resp.Data["keys"].([]interface{})
secret, err := client.Logical().Read(fmt.Sprintf("secretsv2/data/%s", keys[0]))
if secret == nil {
return fmt.Errorf("no secret found at secretsv2/data/%s", keys[0])
}

data := secret.Data["data"].(map[string]interface{})

// Confirm number of versions
err = testResourceGenericSecret_checkVersions(client, keys[0].(string), versionCount)
if err != nil {
return fmt.Errorf("Version error: %s", err)
}

// Test the JSON
if got := data["zip"]; got != wantValue {
return fmt.Errorf("'zip' data is %q; want %q", got, wantValue)
}

// Test the map
if got := instanceState.Attributes["data.zip"]; got != wantValue {
return fmt.Errorf("data[\"zip\"] contains %s; want %s", got, wantValue)
}
return nil
}
}

func testResourceGenericSecret_checkVersions(client *api.Client, keyName string, versionCount int) error {
resp, err := client.Logical().Read(fmt.Sprintf("secretsv2/metadata/%s", keyName))
if err != nil {
return fmt.Errorf("unable to read secrets metadata at path secretsv2/metadata/%s: %s", keyName, err)
}

versions := resp.Data["versions"].(map[string]interface{})

if len(versions) != versionCount {
return fmt.Errorf("Expected %d versions, got %d", versionCount, len(versions))
}

return nil
}

func testAllVersionDestroy(s *terraform.State) error {
client := testProvider.Meta().(*api.Client)

for _, rs := range s.RootModule().Resources {
if rs.Type != "vault_generic_secret" {
continue
}
secret, err := client.Logical().Read(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error checking for generic secret %q: %s", rs.Primary.ID, err)
}
if secret != nil {
return fmt.Errorf("generic secret %q still exists", rs.Primary.ID)
}
}
return nil
}

var testResourceGenericSecret_updateConfig = `

resource "vault_mount" "v1" {
Expand Down
7 changes: 6 additions & 1 deletion website/docs/r/generic_secret.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ The following arguments are supported:
* `data_json` - (Required) String containing a JSON-encoded object that will be
written as the secret data at the given path.

* `disable_read` - (Optional) True/false. Set this to true if your vault
* `disable_read` - (Optional) true/false. Set this to true if your vault
authentication is not able to read the data. Setting this to `true` will
break drift detection. Defaults to false.

* `delete_all_versions` - (Optional) true/false. Only applicable for kv-v2 stores.
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
If set to `true`, permanently deletes all versions for
the specified key. The default behavior is to only delete the latest version of the
secret.

## Required Vault Capabilities

Use of this resource requires the `create` or `update` capability
Expand Down