1package kv
2
3import (
4	"context"
5	"fmt"
6	"strings"
7	"time"
8
9	"github.com/golang/protobuf/ptypes"
10	"github.com/hashicorp/vault/sdk/framework"
11	"github.com/hashicorp/vault/sdk/helper/locksutil"
12	"github.com/hashicorp/vault/sdk/logical"
13)
14
15// pathMetadata returns the path configuration for CRUD operations on the
16// metadata endpoint
17func pathMetadata(b *versionedKVBackend) *framework.Path {
18	return &framework.Path{
19		Pattern: "metadata/" + framework.MatchAllRegex("path"),
20		Fields: map[string]*framework.FieldSchema{
21			"path": {
22				Type:        framework.TypeString,
23				Description: "Location of the secret.",
24			},
25			"cas_required": {
26				Type: framework.TypeBool,
27				Description: `
28If true the key will require the cas parameter to be set on all write requests.
29If false, the backends configuration will be used.`,
30			},
31			"max_versions": {
32				Type: framework.TypeInt,
33				Description: `
34The number of versions to keep. If not set, the backends configured max
35version is used.`,
36			},
37			"delete_version_after": {
38				Type: framework.TypeDurationSecond,
39				Description: `
40The length of time before a version is deleted. If not set, the backend's
41configured delete_version_after is used. Cannot be greater than the
42backend's delete_version_after. A zero duration clears the current setting.
43A negative duration will cause an error.
44`,
45			},
46		},
47		Callbacks: map[logical.Operation]framework.OperationFunc{
48			logical.UpdateOperation: b.upgradeCheck(b.pathMetadataWrite()),
49			logical.CreateOperation: b.upgradeCheck(b.pathMetadataWrite()),
50			logical.ReadOperation:   b.upgradeCheck(b.pathMetadataRead()),
51			logical.DeleteOperation: b.upgradeCheck(b.pathMetadataDelete()),
52			logical.ListOperation:   b.upgradeCheck(b.pathMetadataList()),
53		},
54
55		ExistenceCheck: b.metadataExistenceCheck(),
56
57		HelpSynopsis:    confHelpSyn,
58		HelpDescription: confHelpDesc,
59	}
60}
61
62func (b *versionedKVBackend) metadataExistenceCheck() framework.ExistenceFunc {
63	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
64		key := data.Get("path").(string)
65
66		meta, err := b.getKeyMetadata(ctx, req.Storage, key)
67		if err != nil {
68			// If we are returning a readonly error it means we are attempting
69			// to write the policy for the first time. This means no data exists
70			// yet and we can safely return false here.
71			if strings.Contains(err.Error(), logical.ErrReadOnly.Error()) {
72				return false, nil
73			}
74
75			return false, err
76		}
77
78		return meta != nil, nil
79	}
80}
81
82func (b *versionedKVBackend) pathMetadataList() framework.OperationFunc {
83	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
84		key := data.Get("path").(string)
85
86		// Get an encrypted key storage object
87		wrapper, err := b.getKeyEncryptor(ctx, req.Storage)
88		if err != nil {
89			return nil, err
90		}
91
92		es := wrapper.Wrap(req.Storage)
93
94		// Use encrypted key storage to list the keys
95		keys, err := es.List(ctx, key)
96		return logical.ListResponse(keys), err
97	}
98}
99
100func (b *versionedKVBackend) pathMetadataRead() framework.OperationFunc {
101	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
102		key := data.Get("path").(string)
103
104		meta, err := b.getKeyMetadata(ctx, req.Storage, key)
105		if err != nil {
106			return nil, err
107		}
108		if meta == nil {
109			return nil, nil
110		}
111
112		versions := make(map[string]interface{}, len(meta.Versions))
113		for i, v := range meta.Versions {
114			versions[fmt.Sprintf("%d", i)] = map[string]interface{}{
115				"created_time":  ptypesTimestampToString(v.CreatedTime),
116				"deletion_time": ptypesTimestampToString(v.DeletionTime),
117				"destroyed":     v.Destroyed,
118			}
119		}
120
121		var deleteVersionAfter time.Duration
122		if meta.GetDeleteVersionAfter() != nil {
123			deleteVersionAfter, err = ptypes.Duration(meta.GetDeleteVersionAfter())
124			if err != nil {
125				return nil, err
126			}
127		}
128
129		return &logical.Response{
130			Data: map[string]interface{}{
131				"versions":             versions,
132				"current_version":      meta.CurrentVersion,
133				"oldest_version":       meta.OldestVersion,
134				"created_time":         ptypesTimestampToString(meta.CreatedTime),
135				"updated_time":         ptypesTimestampToString(meta.UpdatedTime),
136				"max_versions":         meta.MaxVersions,
137				"cas_required":         meta.CasRequired,
138				"delete_version_after": deleteVersionAfter.String(),
139			},
140		}, nil
141	}
142}
143
144func (b *versionedKVBackend) pathMetadataWrite() framework.OperationFunc {
145	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
146		key := data.Get("path").(string)
147		if key == "" {
148			return logical.ErrorResponse("missing path"), nil
149		}
150
151		maxRaw, mOk := data.GetOk("max_versions")
152		casRaw, cOk := data.GetOk("cas_required")
153		deleteVersionAfterRaw, dvaOk := data.GetOk("delete_version_after")
154
155		// Fast path validation
156		if !mOk && !cOk && !dvaOk {
157			return nil, nil
158		}
159
160		config, err := b.config(ctx, req.Storage)
161		if err != nil {
162			return nil, err
163		}
164
165		var resp *logical.Response
166		if cOk && config.CasRequired && !casRaw.(bool) {
167			resp = &logical.Response{}
168			resp.AddWarning("\"cas_required\" set to false, but is mandated by backend config. This value will be ignored.")
169		}
170
171		lock := locksutil.LockForKey(b.locks, key)
172		lock.Lock()
173		defer lock.Unlock()
174
175		meta, err := b.getKeyMetadata(ctx, req.Storage, key)
176		if err != nil {
177			return nil, err
178		}
179		if meta == nil {
180			now := ptypes.TimestampNow()
181			meta = &KeyMetadata{
182				Key:         key,
183				Versions:    map[uint64]*VersionMetadata{},
184				CreatedTime: now,
185				UpdatedTime: now,
186			}
187		}
188
189		if mOk {
190			meta.MaxVersions = uint32(maxRaw.(int))
191		}
192		if cOk {
193			meta.CasRequired = casRaw.(bool)
194		}
195		if dvaOk {
196			meta.DeleteVersionAfter = ptypes.DurationProto(time.Duration(deleteVersionAfterRaw.(int)) * time.Second)
197		}
198
199		err = b.writeKeyMetadata(ctx, req.Storage, meta)
200		return resp, err
201	}
202}
203
204func (b *versionedKVBackend) pathMetadataDelete() framework.OperationFunc {
205	return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
206		key := data.Get("path").(string)
207
208		lock := locksutil.LockForKey(b.locks, key)
209		lock.Lock()
210		defer lock.Unlock()
211
212		meta, err := b.getKeyMetadata(ctx, req.Storage, key)
213		if err != nil {
214			return nil, err
215		}
216		if meta == nil {
217			return nil, nil
218		}
219
220		// Delete each version.
221		for id, _ := range meta.Versions {
222			versionKey, err := b.getVersionKey(ctx, key, id, req.Storage)
223			if err != nil {
224				return nil, err
225			}
226
227			err = req.Storage.Delete(ctx, versionKey)
228			if err != nil {
229				return nil, err
230			}
231		}
232
233		// Get an encrypted key storage object
234		wrapper, err := b.getKeyEncryptor(ctx, req.Storage)
235		if err != nil {
236			return nil, err
237		}
238
239		es := wrapper.Wrap(req.Storage)
240
241		// Use encrypted key storage to delete the key
242		err = es.Delete(ctx, key)
243		return nil, err
244	}
245}
246
247const metadataHelpSyn = `Allows interaction with key metadata and settings in the KV store.`
248const metadataHelpDesc = `
249This endpoint allows for reading, information about a key in the key-value
250store, writing key settings, and permanently deleting a key and all versions.
251`
252