KV helpers for DeleteMetadata, Undelete, Destroy, and Rollback (#15637)
* KV helpers for DeleteMetadata, Undelete, Destroy, and Rollback * Allow rollback when no secret data on latest version, and update error messages
This commit is contained in:
parent
e64d7df041
commit
3cfafe619b
120
api/kv_v2.go
120
api/kv_v2.go
|
@ -323,6 +323,98 @@ func (kv *kvv2) DeleteVersions(ctx context.Context, secretPath string, versions
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteMetadata deletes all versions and metadata of the secret at the
|
||||||
|
// given path.
|
||||||
|
func (kv *kvv2) DeleteMetadata(ctx context.Context, secretPath string) error {
|
||||||
|
pathToDelete := fmt.Sprintf("%s/metadata/%s", kv.mountPath, secretPath)
|
||||||
|
|
||||||
|
_, err := kv.c.Logical().DeleteWithContext(ctx, pathToDelete)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error deleting secret metadata at %s: %w", pathToDelete, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undelete undeletes the given versions of a secret, restoring the data
|
||||||
|
// so that it can be fetched again with Get requests.
|
||||||
|
//
|
||||||
|
// A list of existing versions can be retrieved using the GetVersionsAsList method.
|
||||||
|
func (kv *kvv2) Undelete(ctx context.Context, secretPath string, versions []int) error {
|
||||||
|
pathToUndelete := fmt.Sprintf("%s/undelete/%s", kv.mountPath, secretPath)
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"versions": versions,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := kv.c.Logical().WriteWithContext(ctx, pathToUndelete, data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error undeleting secret metadata at %s: %w", pathToUndelete, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy permanently removes the specified secret versions' data
|
||||||
|
// from the Vault server. If no secret exists at the given path, no
|
||||||
|
// action will be taken.
|
||||||
|
//
|
||||||
|
// A list of existing versions can be retrieved using the GetVersionsAsList method.
|
||||||
|
func (kv *kvv2) Destroy(ctx context.Context, secretPath string, versions []int) error {
|
||||||
|
pathToDestroy := fmt.Sprintf("%s/destroy/%s", kv.mountPath, secretPath)
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"versions": versions,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := kv.c.Logical().WriteWithContext(ctx, pathToDestroy, data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error destroying secret metadata at %s: %w", pathToDestroy, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback can be used to roll a secret back to a previous
|
||||||
|
// non-deleted/non-destroyed version. That previous version becomes the
|
||||||
|
// next/newest version for the path.
|
||||||
|
func (kv *kvv2) Rollback(ctx context.Context, secretPath string, toVersion int) (*KVSecret, error) {
|
||||||
|
// First, do a read to get the current version for check-and-set
|
||||||
|
latest, err := kv.Get(ctx, secretPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get latest version of secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure a value already exists
|
||||||
|
if latest == nil {
|
||||||
|
return nil, fmt.Errorf("no secret was found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify metadata found
|
||||||
|
if latest.VersionMetadata == nil {
|
||||||
|
return nil, fmt.Errorf("no metadata found; rollback can only be used on existing data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now run it again and read the version we want to roll back to
|
||||||
|
rollbackVersion, err := kv.GetVersion(ctx, secretPath, toVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get previous version %d of secret: %s", toVersion, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateRollbackVersion(rollbackVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid rollback version %d: %w", toVersion, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
casVersion := latest.VersionMetadata.Version
|
||||||
|
kvs, err := kv.Put(ctx, secretPath, rollbackVersion.Data, WithCheckAndSet(casVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to roll back to previous secret version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kvs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func extractCustomMetadata(secret *Secret) (map[string]interface{}, error) {
|
func extractCustomMetadata(secret *Secret) (map[string]interface{}, error) {
|
||||||
// Logical Writes return the metadata directly, Reads return it nested inside the "metadata" key
|
// Logical Writes return the metadata directly, Reads return it nested inside the "metadata" key
|
||||||
cmI, ok := secret.Data["custom_metadata"]
|
cmI, ok := secret.Data["custom_metadata"]
|
||||||
|
@ -468,6 +560,34 @@ func extractFullMetadata(secret *Secret) (*KVMetadata, error) {
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateRollbackVersion(rollbackVersion *KVSecret) error {
|
||||||
|
// Make sure a value already exists
|
||||||
|
if rollbackVersion == nil || rollbackVersion.Data == nil {
|
||||||
|
return fmt.Errorf("no secret found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify metadata found
|
||||||
|
if rollbackVersion.VersionMetadata == nil {
|
||||||
|
return fmt.Errorf("no version metadata found; rollback only works on existing data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it hasn't been deleted
|
||||||
|
if !rollbackVersion.VersionMetadata.DeletionTime.IsZero() {
|
||||||
|
return fmt.Errorf("cannot roll back to a version that has been deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rollbackVersion.VersionMetadata.Destroyed {
|
||||||
|
return fmt.Errorf("cannot roll back to a version that has been destroyed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify old data found
|
||||||
|
if rollbackVersion.Data == nil {
|
||||||
|
return fmt.Errorf("no data found; rollback only works on existing data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func mergePatch(ctx context.Context, client *Client, mountPath string, secretPath string, newData map[string]interface{}, opts ...KVOption) (*KVSecret, error) {
|
func mergePatch(ctx context.Context, client *Client, mountPath string, secretPath string, newData map[string]interface{}, opts ...KVOption) (*KVSecret, error) {
|
||||||
pathToMergePatch := fmt.Sprintf("%s/data/%s", mountPath, secretPath)
|
pathToMergePatch := fmt.Sprintf("%s/data/%s", mountPath, secretPath)
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,12 @@ func TestKVHelpers(t *testing.T) {
|
||||||
t.Fatalf("data still exists on the first version of the secret despite this version being deleted")
|
t.Fatalf("data still exists on the first version of the secret despite this version being deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// undelete it
|
||||||
|
err = client.KVv2(mountPath).Undelete(context.Background(), secretPath, []int{1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// check that KVOption works
|
// check that KVOption works
|
||||||
////
|
////
|
||||||
// WithCheckAndSet
|
// WithCheckAndSet
|
||||||
|
@ -295,5 +301,40 @@ func TestKVHelpers(t *testing.T) {
|
||||||
if versions[0].Version != 1 || versions[len(versions)-1].Version != expectedLength {
|
if versions[0].Version != 1 || versions[len(versions)-1].Version != expectedLength {
|
||||||
t.Fatalf("versions list is not ordered as expected")
|
t.Fatalf("versions list is not ordered as expected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roll back to version 1
|
||||||
|
rb, err := client.KVv2(mountPath).Rollback(context.Background(), secretPath, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if rb.VersionMetadata.Version != 9 {
|
||||||
|
t.Fatalf("expected returned secret's version %d to be the latest version, which should be 9", rb.VersionMetadata.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy version 1
|
||||||
|
err = client.KVv2(mountPath).Destroy(context.Background(), secretPath, []int{1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// roll back but fail
|
||||||
|
_, err = client.KVv2(mountPath).Rollback(context.Background(), secretPath, 1)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error from trying to rollback to destroyed version")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create another secret
|
||||||
|
_, err = client.KVv2(mountPath).Put(context.Background(), "nested/my-other-secret", map[string]interface{}{
|
||||||
|
"color": "yellow",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, delete it all
|
||||||
|
err = client.KVv2(mountPath).DeleteMetadata(context.Background(), secretPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue