Skip to content

Commit

Permalink
Handle out-of-band approle deletion (#2142)
Browse files Browse the repository at this point in the history
  • Loading branch information
vinay-gopalan committed Feb 14, 2024
1 parent 17a91c8 commit e8197cf
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 54 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

BUGS:
* Handle graceful destruction of resources when approle is deleted out-of-band ([#2142](https://github.com/hashicorp/terraform-provider-vault/pull/2142)).

## 3.25.0 (Feb 14, 2024)

FEATURES:
Expand Down
4 changes: 4 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ const (
FieldOptions = "options"
FieldAllowedManagedKeys = "allowed_managed_keys"
FieldIdentityTokenKey = "identity_token_key"
FieldCIDRList = "cidr_list"
FieldSecretID = "secret_id"
FieldWrappingToken = "wrapping_token"
FieldWithWrappedAccessor = "with_wrapped_accessor"

/*
common environment variables
Expand Down
139 changes: 85 additions & 54 deletions vault/resource_approle_auth_backend_role_secret_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package vault

import (
"context"
"encoding/json"
"fmt"
"log"
"regexp"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/hashicorp/terraform-provider-vault/internal/consts"
Expand All @@ -21,20 +23,19 @@ var approleAuthBackendRoleSecretIDIDRegex = regexp.MustCompile("^backend=(.+)::r

func approleAuthBackendRoleSecretIDResource(name string) *schema.Resource {
return &schema.Resource{
Create: approleAuthBackendRoleSecretIDCreate,
Read: provider.ReadWrapper(approleAuthBackendRoleSecretIDRead),
Delete: approleAuthBackendRoleSecretIDDelete,
Exists: approleAuthBackendRoleSecretIDExists,
CreateContext: approleAuthBackendRoleSecretIDCreate,
ReadContext: provider.ReadContextWrapper(approleAuthBackendRoleSecretIDRead),
DeleteContext: approleAuthBackendRoleSecretIDDelete,

Schema: map[string]*schema.Schema{
"role_name": {
consts.FieldRoleName: {
Type: schema.TypeString,
Required: true,
Description: "Name of the role.",
ForceNew: true,
},

"secret_id": {
consts.FieldSecretID: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Expand All @@ -43,7 +44,7 @@ func approleAuthBackendRoleSecretIDResource(name string) *schema.Resource {
Sensitive: true,
},

"cidr_list": {
consts.FieldCIDRList: {
Type: schema.TypeSet,
Optional: true,
Description: "List of CIDR blocks that can log in using the SecretID.",
Expand Down Expand Up @@ -71,7 +72,7 @@ func approleAuthBackendRoleSecretIDResource(name string) *schema.Resource {
},
},

"backend": {
consts.FieldBackend: {
Type: schema.TypeString,
Optional: true,
Description: "Unique name of the auth backend to configure.",
Expand All @@ -83,35 +84,35 @@ func approleAuthBackendRoleSecretIDResource(name string) *schema.Resource {
},
},

"with_wrapped_accessor": {
consts.FieldWithWrappedAccessor: {
Type: schema.TypeBool,
Optional: true,
Description: "Use the wrapped secret-id accessor as the id of this resource. If false, a fresh secret-id will be regenerated whenever the wrapping token is expired or invalidated through unwrapping.",
ForceNew: true,
},

"accessor": {
consts.FieldAccessor: {
Type: schema.TypeString,
Computed: true,
Description: "The unique ID used to access this SecretID.",
},

"wrapping_ttl": {
consts.FieldWrappingTTL: {
Type: schema.TypeString,
Required: false,
Optional: true,
ForceNew: true,
Description: "The TTL duration of the wrapped SecretID.",
},

"wrapping_token": {
consts.FieldWrappingToken: {
Type: schema.TypeString,
Computed: true,
Description: "The wrapped SecretID token.",
Sensitive: true,
},

"wrapping_accessor": {
consts.FieldWrappingAccessor: {
Type: schema.TypeString,
Computed: true,
Description: "The wrapped SecretID accessor.",
Expand All @@ -120,56 +121,56 @@ func approleAuthBackendRoleSecretIDResource(name string) *schema.Resource {
}
}

func approleAuthBackendRoleSecretIDCreate(d *schema.ResourceData, meta interface{}) error {
func approleAuthBackendRoleSecretIDCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

backend := d.Get("backend").(string)
role := d.Get("role_name").(string)
backend := d.Get(consts.FieldBackend).(string)
role := d.Get(consts.FieldRoleName).(string)

path := approleAuthBackendRolePath(backend, role) + "/secret-id"

if _, ok := d.GetOk("secret_id"); ok {
if _, ok := d.GetOk(consts.FieldSecretID); ok {
path = approleAuthBackendRolePath(backend, role) + "/custom-secret-id"
}

log.Printf("[DEBUG] Writing AppRole auth backend role SecretID %q", path)
iCIDRs := d.Get("cidr_list").(*schema.Set).List()
iCIDRs := d.Get(consts.FieldCIDRList).(*schema.Set).List()
cidrs := make([]string, 0, len(iCIDRs))
for _, iCIDR := range iCIDRs {
cidrs = append(cidrs, iCIDR.(string))
}

data := map[string]interface{}{}
if v, ok := d.GetOk("secret_id"); ok {
data["secret_id"] = v.(string)
if v, ok := d.GetOk(consts.FieldSecretID); ok {
data[consts.FieldSecretID] = v.(string)
}
if len(cidrs) > 0 {
data["cidr_list"] = strings.Join(cidrs, ",")
data[consts.FieldCIDRList] = strings.Join(cidrs, ",")
}
if v, ok := d.GetOk(consts.FieldMetadata); ok {
name := "vault_approle_auth_backend_role_secret_id"
result, err := normalizeDataJSON(v.(string))
if err != nil {
log.Printf("[ERROR] Failed to normalize JSON data %q, resource=%q, key=%q, err=%s",
v, name, "metadata", err)
return err
return diag.FromErr(err)
}
data["metadata"] = result
} else {
data["metadata"] = ""
}
withWrappedAccessor := d.Get("with_wrapped_accessor").(bool)
withWrappedAccessor := d.Get(consts.FieldWithWrappedAccessor).(bool)

wrappingTTL, wrapped := d.GetOk("wrapping_ttl")
wrappingTTL, wrapped := d.GetOk(consts.FieldWrappingTTL)

if wrapped {
var err error

if client, err = client.Clone(); err != nil {
return fmt.Errorf("error cloning client: %w", err)
return diag.Errorf("error cloning client: %s", err)
}
client.SetWrappingLookupFunc(func(_, _ string) string {
return wrappingTTL.(string)
Expand All @@ -178,7 +179,7 @@ func approleAuthBackendRoleSecretIDCreate(d *schema.ResourceData, meta interface

resp, err := client.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("error writing AppRole auth backend role SecretID %q: %s", path, err)
return diag.Errorf("error writing AppRole auth backend role SecretID %q: %s", path, err)
}
log.Printf("[DEBUG] Wrote AppRole auth backend role SecretID %q", path)

Expand All @@ -190,40 +191,49 @@ func approleAuthBackendRoleSecretIDCreate(d *schema.ResourceData, meta interface
} else {
accessor = resp.WrapInfo.Accessor
}
d.Set("wrapping_token", resp.WrapInfo.Token)
d.Set("wrapping_accessor", accessor)
if err := d.Set(consts.FieldWrappingToken, resp.WrapInfo.Token); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldWrappingAccessor, accessor); err != nil {
return diag.FromErr(err)
}
} else {
accessor = resp.Data["secret_id_accessor"].(string)
d.Set("secret_id", resp.Data["secret_id"])
d.Set("accessor", accessor)
if err := d.Set(consts.FieldSecretID, resp.Data[consts.FieldSecretID]); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldAccessor, accessor); err != nil {
return diag.FromErr(err)
}

}

d.SetId(approleAuthBackendRoleSecretIDID(backend, role, accessor, wrapped, withWrappedAccessor))

return approleAuthBackendRoleSecretIDRead(d, meta)
return approleAuthBackendRoleSecretIDRead(ctx, d, meta)
}

func approleAuthBackendRoleSecretIDRead(d *schema.ResourceData, meta interface{}) error {
func approleAuthBackendRoleSecretIDRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

id := d.Id()

backend, role, accessor, wrapped, err := approleAuthBackendRoleSecretIDParseID(id)
if err != nil {
return fmt.Errorf("invalid ID %q for AppRole auth backend role SecretID: %s", id, err)
return diag.Errorf("invalid ID %q for AppRole auth backend role SecretID: %s", id, err)
}

// If the ID is wrapped, there is no information available other than whether
// the wrapping token is still valid, unless we are planning to re-use it.
withWrappedAccessor := d.Get("with_wrapped_accessor").(bool)
withWrappedAccessor := d.Get(consts.FieldWithWrappedAccessor).(bool)

if wrapped && !withWrappedAccessor {
valid, err := approleAuthBackendRoleSecretIDExists(d, meta)
if err != nil {
return err
return diag.FromErr(err)
}
if !valid {
log.Printf("[WARN] AppRole auth backend role SecretID %q not found, removing from state", id)
Expand All @@ -243,7 +253,14 @@ func approleAuthBackendRoleSecretIDRead(d *schema.ResourceData, meta interface{}
if util.IsExpiredTokenErr(err) {
return nil
}
return fmt.Errorf("error reading AppRole auth backend role SecretID %q: %s", id, err)
if isAppRoleDoesNotExistError(err, role) {
// remove secretID from state in case approle is deleted out-of-band
log.Printf("[WARN] AppRole auth backend role %q deleted out-of-band, removing secret ID %q from state", role, id)
d.SetId("")
return nil
}

return diag.Errorf("error reading AppRole auth backend role SecretID %q: %s", id, err)
}
log.Printf("[DEBUG] Read AppRole auth backend role SecretID %q", id)
if resp == nil {
Expand All @@ -253,7 +270,7 @@ func approleAuthBackendRoleSecretIDRead(d *schema.ResourceData, meta interface{}
}

var cidrs []string
switch data := resp.Data["cidr_list"].(type) {
switch data := resp.Data[consts.FieldCIDRList].(type) {
case string:
if data != "" {
cidrs = strings.Split(data, ",")
Expand All @@ -266,43 +283,48 @@ func approleAuthBackendRoleSecretIDRead(d *schema.ResourceData, meta interface{}
case nil:
cidrs = make([]string, 0)
default:
return fmt.Errorf("unknown type %T for cidr_list in response for SecretID %q", data, accessor)
return diag.Errorf("unknown type %T for cidr_list in response for SecretID %q", data, accessor)
}

metadata, err := json.Marshal(resp.Data["metadata"])
if err != nil {
return fmt.Errorf("error encoding metadata for SecretID %q to JSON: %s", id, err)
return diag.Errorf("error encoding metadata for SecretID %q to JSON: %s", id, err)
}

d.Set("backend", backend)
d.Set("role_name", role)
err = d.Set("cidr_list", cidrs)
if err != nil {
return fmt.Errorf("error setting cidr_list in state: %s", err)
fields := map[string]interface{}{
consts.FieldBackend: backend,
consts.FieldRoleName: role,
consts.FieldCIDRList: cidrs,
consts.FieldMetadata: string(metadata),
consts.FieldAccessor: accessor,
}

for k, v := range fields {
if err := d.Set(k, v); err != nil {
return diag.Errorf("error setting %q in state; err=%s", k, err)
}
}
d.Set(consts.FieldMetadata, string(metadata))
d.Set("accessor", accessor)

return nil
}

func approleAuthBackendRoleSecretIDDelete(d *schema.ResourceData, meta interface{}) error {
func approleAuthBackendRoleSecretIDDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

id := d.Id()
backend, role, accessor, wrapped, err := approleAuthBackendRoleSecretIDParseID(id)
if err != nil {
return fmt.Errorf("invalid ID %q for AppRole auth backend role SecretID: %s", id, err)
return diag.Errorf("invalid ID %q for AppRole auth backend role SecretID: %s", id, err)
}

var path, accessorParam string

if wrapped {
path = "auth/token/revoke-accessor"
accessorParam = "accessor"
accessorParam = consts.FieldAccessor
} else {
path = approleAuthBackendRolePath(backend, role) + "/secret-id-accessor/destroy"
accessorParam = "secret_id_accessor"
Expand All @@ -313,7 +335,7 @@ func approleAuthBackendRoleSecretIDDelete(d *schema.ResourceData, meta interface
accessorParam: accessor,
})
if err != nil {
return fmt.Errorf("error deleting AppRole auth backend role SecretID %q", id)
return diag.Errorf("error deleting AppRole auth backend role SecretID %q", id)
}
log.Printf("[DEBUG] Deleted AppRole auth backend role SecretID %q", id)

Expand All @@ -335,7 +357,7 @@ func approleAuthBackendRoleSecretIDExists(d *schema.ResourceData, meta interface

if wrapped {
_, err := client.Logical().Write("auth/token/lookup-accessor", map[string]interface{}{
"accessor": accessor,
consts.FieldAccessor: accessor,
})
if err == nil {
return true, nil
Expand All @@ -356,6 +378,11 @@ func approleAuthBackendRoleSecretIDExists(d *schema.ResourceData, meta interface
if util.IsExpiredTokenErr(err) || util.Is404(err) {
return false, nil
}

if isAppRoleDoesNotExistError(err, role) {
// secretID is invalid if approle is deleted out-of-band
return false, nil
}
return true, fmt.Errorf("error checking if AppRole auth backend role SecretID %q exists: %s", id, err)
}
log.Printf("[DEBUG] Checked if AppRole auth backend role SecretID %q exists", id)
Expand Down Expand Up @@ -388,3 +415,7 @@ func approleAuthBackendRoleSecretIDParseID(id string) (backend, role, accessor s

return
}

func isAppRoleDoesNotExistError(err error, role string) bool {
return util.Is500(err) && strings.Contains(err.Error(), fmt.Sprintf("role \"%s\" does not exist", role))
}
Loading

0 comments on commit e8197cf

Please sign in to comment.