1package vault
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	"github.com/hashicorp/vault/sdk/framework"
10	"github.com/hashicorp/vault/sdk/helper/jsonutil"
11	"github.com/hashicorp/vault/sdk/logical"
12)
13
14// CubbyholeBackendFactory constructs a new cubbyhole backend
15func CubbyholeBackendFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
16	b := &CubbyholeBackend{}
17	b.Backend = &framework.Backend{
18		Help: strings.TrimSpace(cubbyholeHelp),
19	}
20
21	b.Backend.Paths = append(b.Backend.Paths, b.paths()...)
22
23	if conf == nil {
24		return nil, fmt.Errorf("configuration passed into backend is nil")
25	}
26	b.Backend.Setup(ctx, conf)
27
28	return b, nil
29}
30
31// CubbyholeBackend is used for storing secrets directly into the physical
32// backend. The secrets are encrypted in the durable storage.
33// This differs from kv in that every token has its own private
34// storage view. The view is removed when the token expires.
35type CubbyholeBackend struct {
36	*framework.Backend
37
38	saltUUID    string
39	storageView logical.Storage
40}
41
42func (b *CubbyholeBackend) paths() []*framework.Path {
43	return []*framework.Path{
44		{
45			Pattern: framework.MatchAllRegex("path"),
46
47			Fields: map[string]*framework.FieldSchema{
48				"path": {
49					Type:        framework.TypeString,
50					Description: "Specifies the path of the secret.",
51				},
52			},
53
54			Operations: map[logical.Operation]framework.OperationHandler{
55				logical.ReadOperation: &framework.PathOperation{
56					Callback: b.handleRead,
57					Summary:  "Retrieve the secret at the specified location.",
58				},
59				logical.UpdateOperation: &framework.PathOperation{
60					Callback: b.handleWrite,
61					Summary:  "Store a secret at the specified location.",
62				},
63				logical.CreateOperation: &framework.PathOperation{
64					Callback: b.handleWrite,
65				},
66				logical.DeleteOperation: &framework.PathOperation{
67					Callback: b.handleDelete,
68					Summary:  "Deletes the secret at the specified location.",
69				},
70				logical.ListOperation: &framework.PathOperation{
71					Callback:    b.handleList,
72					Summary:     "List secret entries at the specified location.",
73					Description: "Folders are suffixed with /. The input must be a folder; list on a file will not return a value. The values themselves are not accessible via this command.",
74				},
75			},
76
77			ExistenceCheck: b.handleExistenceCheck,
78
79			HelpSynopsis:    strings.TrimSpace(cubbyholeHelpSynopsis),
80			HelpDescription: strings.TrimSpace(cubbyholeHelpDescription),
81		},
82	}
83}
84
85func (b *CubbyholeBackend) revoke(ctx context.Context, view *BarrierView, saltedToken string) error {
86	if saltedToken == "" {
87		return fmt.Errorf("client token empty during revocation")
88	}
89
90	if err := logical.ClearView(ctx, view.SubView(saltedToken+"/")); err != nil {
91		return err
92	}
93
94	return nil
95}
96
97func (b *CubbyholeBackend) handleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
98	out, err := req.Storage.Get(ctx, req.ClientToken+"/"+req.Path)
99	if err != nil {
100		return false, fmt.Errorf("existence check failed: %w", err)
101	}
102
103	return out != nil, nil
104}
105
106func (b *CubbyholeBackend) handleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
107	if req.ClientToken == "" {
108		return nil, fmt.Errorf("client token empty")
109	}
110
111	path := data.Get("path").(string)
112
113	if path == "" {
114		return nil, fmt.Errorf("missing path")
115	}
116
117	// Read the path
118	out, err := req.Storage.Get(ctx, req.ClientToken+"/"+path)
119	if err != nil {
120		return nil, fmt.Errorf("read failed: %w", err)
121	}
122
123	// Fast-path the no data case
124	if out == nil {
125		return nil, nil
126	}
127
128	// Decode the data
129	var rawData map[string]interface{}
130	if err := jsonutil.DecodeJSON(out.Value, &rawData); err != nil {
131		return nil, fmt.Errorf("json decoding failed: %w", err)
132	}
133
134	// Generate the response
135	resp := &logical.Response{
136		Data: rawData,
137	}
138
139	return resp, nil
140}
141
142func (b *CubbyholeBackend) handleWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
143	if req.ClientToken == "" {
144		return nil, fmt.Errorf("client token empty")
145	}
146	// Check that some fields are given
147	if len(req.Data) == 0 {
148		return nil, fmt.Errorf("missing data fields")
149	}
150
151	path := data.Get("path").(string)
152
153	if path == "" {
154		return nil, fmt.Errorf("missing path")
155	}
156
157	// JSON encode the data
158	buf, err := json.Marshal(req.Data)
159	if err != nil {
160		return nil, fmt.Errorf("json encoding failed: %w", err)
161	}
162
163	// Write out a new key
164	entry := &logical.StorageEntry{
165		Key:   req.ClientToken + "/" + path,
166		Value: buf,
167	}
168	if req.WrapInfo != nil && req.WrapInfo.SealWrap {
169		entry.SealWrap = true
170	}
171	if err := req.Storage.Put(ctx, entry); err != nil {
172		return nil, fmt.Errorf("failed to write: %w", err)
173	}
174
175	return nil, nil
176}
177
178func (b *CubbyholeBackend) handleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
179	if req.ClientToken == "" {
180		return nil, fmt.Errorf("client token empty")
181	}
182
183	path := data.Get("path").(string)
184
185	// Delete the key at the request path
186	if err := req.Storage.Delete(ctx, req.ClientToken+"/"+path); err != nil {
187		return nil, err
188	}
189
190	return nil, nil
191}
192
193func (b *CubbyholeBackend) handleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
194	if req.ClientToken == "" {
195		return nil, fmt.Errorf("client token empty")
196	}
197
198	// Right now we only handle directories, so ensure it ends with / We also
199	// check if it's empty so we don't end up doing a listing on '<client
200	// token>//'
201	path := data.Get("path").(string)
202	if path != "" && !strings.HasSuffix(path, "/") {
203		path = path + "/"
204	}
205
206	// List the keys at the prefix given by the request
207	keys, err := req.Storage.List(ctx, req.ClientToken+"/"+path)
208	if err != nil {
209		return nil, err
210	}
211
212	// Strip the token
213	strippedKeys := make([]string, len(keys))
214	for i, key := range keys {
215		strippedKeys[i] = strings.TrimPrefix(key, req.ClientToken+"/")
216	}
217
218	// Generate the response
219	return logical.ListResponse(strippedKeys), nil
220}
221
222const cubbyholeHelp = `
223The cubbyhole backend reads and writes arbitrary secrets to the backend.
224The secrets are encrypted/decrypted by Vault: they are never stored
225unencrypted in the backend and the backend never has an opportunity to
226see the unencrypted value.
227
228This backend differs from the 'kv' backend in that it is namespaced
229per-token. Tokens can only read and write their own values, with no
230sharing possible (per-token cubbyholes). This can be useful for implementing
231certain authentication workflows, as well as "scratch" areas for individual
232clients. When the token is revoked, the entire set of stored values for that
233token is also removed.
234`
235
236const cubbyholeHelpSynopsis = `
237Pass-through secret storage to a token-specific cubbyhole in the storage
238backend, allowing you to read/write arbitrary data into secret storage.
239`
240
241const cubbyholeHelpDescription = `
242The cubbyhole backend reads and writes arbitrary data into secret storage,
243encrypting it along the way.
244
245The view into the cubbyhole storage space is different for each token; it is
246a per-token cubbyhole. When the token is revoked all values are removed.
247`
248