Skip to content

Commit 6d62412

Browse files
committed
support create-version operation
The `create-version` operation Creates a specific version of a secret, sets its value and immediately activates that version. It fails with HTTP status 412 (precondition failed) if this version of the secret was ever set, even if it has since been deleted.. Updates tailscale/corp#34020 Signed-off-by: Percy Wegmann <[email protected]>
1 parent 2ab774e commit 6d62412

File tree

11 files changed

+332
-5
lines changed

11 files changed

+332
-5
lines changed

acl/acl.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ const (
2727
// secret.
2828
ActionPut = Action("put")
2929

30+
// ActionCreateVersion ("create-version" in the API) denotes permission to
31+
// create a specific version of a secret if and only if that version has never
32+
// been set.
33+
ActionCreateVersion = Action("create-version")
34+
3035
// ActionActivate ("activate" in the API) denotes permission to set one one
3136
// of of the available versions of a secret as the active one.
3237
ActionActivate = Action("activate")

acl/acl_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestACL(t *testing.T) {
1616
Secret: []acl.Secret{"control/foo", "control/bar"},
1717
},
1818
acl.Rule{
19-
Action: []acl.Action{acl.ActionInfo, acl.ActionPut, acl.ActionActivate},
19+
Action: []acl.Action{acl.ActionInfo, acl.ActionPut, acl.ActionCreateVersion, acl.ActionActivate},
2020
Secret: []acl.Secret{"*"},
2121
},
2222
acl.Rule{
@@ -55,6 +55,12 @@ func TestACL(t *testing.T) {
5555
allow("put", "something/else"),
5656
allow("put", "dev/foo"),
5757

58+
allow("create-version", "control/foo"),
59+
allow("create-version", "control/bar"),
60+
allow("create-version", "control/quux"),
61+
allow("create-version", "something/else"),
62+
allow("create-version", "dev/foo"),
63+
5864
allow("activate", "control/foo"),
5965
allow("activate", "control/bar"),
6066
allow("activate", "control/quux"),

client/setec/client.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,19 @@ func (c Client) Put(ctx context.Context, name string, value []byte) (version api
199199
})
200200
}
201201

202+
// CreateVersion Creates a specific version of a secret, sets its value and immediately activates that version.
203+
// It fails if this version of the secret ever had a value.
204+
//
205+
// Access requirement: "create-version"
206+
func (c Client) CreateVersion(ctx context.Context, name string, version api.SecretVersion, value []byte) error {
207+
_, err := do[struct{}](ctx, c, "/api/create-version", api.CreateVersionRequest{
208+
Name: name,
209+
Version: version,
210+
Value: value,
211+
})
212+
return err
213+
}
214+
202215
// Activate changes the active version of the secret called name to version.
203216
//
204217
// Access requirement: "activate"

db/db.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ var (
5050
// ErrNotFound is the error returned by DB methods when the
5151
// database lacks a necessary secret or secret version.
5252
ErrNotFound = errors.New("not found")
53+
// ErrVersionTaken indicates that an attempt was made to create a
54+
// version of a secret that has at some point already been set,
55+
// even if it has since been deleted.
56+
ErrVersionTaken = errors.New("version is (or was previously) set")
57+
// ErrInvalidVersion indicates that an attempt was made to create a
58+
// version of a secret using an invalid version number (<=0).
59+
ErrInvalidVersion = errors.New("invalid version")
5360
)
5461

5562
// Open loads the secrets database at path, decrypting it using key.
@@ -247,6 +254,30 @@ func (db *DB) putConfigLocked(name string, value []byte) (api.SecretVersion, err
247254
}
248255
}
249256

257+
// CreateVersion creates the specified version of the secret called name with
258+
// the specified value. For a secret that does not yet exist, CreateVersion creates
259+
// the secret, sets the specified version to the given value and makes this the
260+
// secret's initial version. For a secret that already exists, CreateVersion
261+
// returns an error if the specified version ever had a value; otherwise, CreateVersion
262+
// sets the specified version to the given value and immediately activates this version.
263+
//
264+
// Access requirement: "create-version"
265+
func (db *DB) CreateVersion(caller Caller, name string, version api.SecretVersion, value []byte) error {
266+
if name == "" {
267+
return errors.New("empty secret name")
268+
}
269+
if version <= 0 {
270+
return ErrInvalidVersion
271+
}
272+
if err := db.checkAndLog(caller, acl.ActionCreateVersion, name, version); err != nil {
273+
return err
274+
}
275+
276+
db.mu.Lock()
277+
defer db.mu.Unlock()
278+
return db.kv.createVersion(name, version, value)
279+
}
280+
250281
// Activate changes the active version of the secret called name to version.
251282
func (db *DB) Activate(caller Caller, name string, version api.SecretVersion) error {
252283
if name == "" {

db/db_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"strconv"
1212
"testing"
13+
"time"
1314

1415
"github.com/google/go-cmp/cmp"
1516
"github.com/tailscale/setec/audit"
@@ -227,6 +228,165 @@ func TestPut(t *testing.T) {
227228
mustGetVersion(ver3, "test value 2")
228229
}
229230

231+
func TestCreateVersion(t *testing.T) {
232+
secretName := "secret1"
233+
checkVersion := func(d *setectest.DB, version api.SecretVersion, want []byte) *api.SecretValue {
234+
t.Helper()
235+
got := d.MustGetVersion(d.Superuser, secretName, version)
236+
if !bytes.Equal(got.Value, want) {
237+
t.Fatalf("Get %q version %v: got %q, want %q", secretName, version, string(got.Value), string(want))
238+
}
239+
return got
240+
}
241+
checkActiveVersion := func(d *setectest.DB, want []byte) {
242+
t.Helper()
243+
got := d.MustGet(d.Superuser, secretName)
244+
if !bytes.Equal(got.Value, want) {
245+
t.Fatalf("Get active %q: got %q, want %q", secretName, string(got.Value), string(want))
246+
}
247+
}
248+
249+
testValue1 := []byte("test value 1")
250+
testValue2 := []byte("test value 2")
251+
testValue3 := []byte("test value 3")
252+
testValue4 := []byte("test value 4")
253+
254+
// One use for setting explicit versions is doing time-based rotation using something like
255+
// UNIX timestamps. This simulates that.
256+
year2099 := api.SecretVersion(time.Date(2099, 12, 31, 24, 60, 60, 0, time.UTC).Unix())
257+
258+
t.Run("create", func(t *testing.T) {
259+
d := setectest.NewDB(t, nil)
260+
261+
// Creating first version should be allowed.
262+
err := d.Actual.CreateVersion(d.Superuser, secretName, 1, testValue1)
263+
if err != nil {
264+
t.Fatalf("failed to create first version: %s", err)
265+
}
266+
checkVersion(d, 1, testValue1)
267+
checkActiveVersion(d, testValue1)
268+
269+
// Creating a disjoint version for the first time should be allowed.
270+
err = d.Actual.CreateVersion(d.Superuser, secretName, year2099, testValue2)
271+
if err != nil {
272+
t.Fatalf("failed to create disjoint version: %s", err)
273+
}
274+
checkVersion(d, year2099, testValue2)
275+
checkActiveVersion(d, testValue2)
276+
277+
// Creating a disjoint version for the second time should not be allowed.
278+
err = d.Actual.CreateVersion(d.Superuser, secretName, year2099, testValue2)
279+
if !errors.Is(err, db.ErrVersionTaken) {
280+
t.Fatalf("Setting existing version should have failed with %q but returned %q", db.ErrVersionTaken, err)
281+
}
282+
checkVersion(d, year2099, testValue2)
283+
checkActiveVersion(d, testValue2)
284+
285+
// Creating an in-between version should be allowed.
286+
err = d.Actual.CreateVersion(d.Superuser, secretName, 100, testValue3)
287+
if err != nil {
288+
t.Fatalf("failed to create in-between version: %s", err)
289+
}
290+
checkVersion(d, 100, testValue3)
291+
checkActiveVersion(d, testValue3)
292+
})
293+
294+
t.Run("create_disjoint_allowed", func(t *testing.T) {
295+
d := setectest.NewDB(t, nil)
296+
297+
// Creating with disjoint version should be allowed.
298+
err := d.Actual.CreateVersion(d.Superuser, secretName, 100, testValue1)
299+
if err != nil {
300+
t.Fatalf("failed to create first version: %s", err)
301+
}
302+
checkVersion(d, 100, testValue1)
303+
checkActiveVersion(d, testValue1)
304+
})
305+
306+
t.Run("create_zero_prohibited", func(t *testing.T) {
307+
d := setectest.NewDB(t, nil)
308+
309+
// Creating with disjoint version should be allowed.
310+
err := d.Actual.CreateVersion(d.Superuser, secretName, 0, testValue1)
311+
if !errors.Is(err, db.ErrInvalidVersion) {
312+
t.Fatalf("Setting version to 0 should have failed with %q but returned %q", db.ErrInvalidVersion, err)
313+
}
314+
})
315+
316+
t.Run("put_create_put_delete", func(t *testing.T) {
317+
d := setectest.NewDB(t, nil)
318+
319+
// Putting a new secret works
320+
version, err := d.Actual.Put(d.Superuser, secretName, testValue1)
321+
if err != nil {
322+
t.Fatalf("failed to Put new secret: %s", err)
323+
}
324+
if version != 1 {
325+
t.Fatalf("expected first Put to create version 1, but created %d", version)
326+
}
327+
checkVersion(d, 1, testValue1)
328+
checkActiveVersion(d, testValue1)
329+
330+
// Creating a higher version works
331+
err = d.Actual.CreateVersion(d.Superuser, secretName, 100, testValue3)
332+
if err != nil {
333+
t.Fatalf("failed to create higher version: %s", err)
334+
}
335+
checkVersion(d, 100, testValue3)
336+
checkActiveVersion(d, testValue3)
337+
338+
// Creating an in-between version works
339+
err = d.Actual.CreateVersion(d.Superuser, secretName, 10, testValue2)
340+
if err != nil {
341+
t.Fatalf("failed to create in-between version: %s", err)
342+
}
343+
checkVersion(d, 10, testValue2)
344+
checkActiveVersion(d, testValue2)
345+
346+
// Putting gets the next higher version, but without activating
347+
version, err = d.Actual.Put(d.Superuser, secretName, testValue4)
348+
if err != nil {
349+
t.Fatalf("failed to Put new secret: %s", err)
350+
}
351+
if version != 101 {
352+
t.Fatalf("expected second Put to create version 101, but created %d", version)
353+
}
354+
checkVersion(d, 101, testValue4)
355+
checkActiveVersion(d, testValue2)
356+
357+
// Deleting highest version allowed
358+
err = d.Actual.DeleteVersion(d.Superuser, secretName, 101)
359+
if err != nil {
360+
t.Fatalf("failed to delete highest version: %s", err)
361+
}
362+
checkActiveVersion(d, testValue2)
363+
364+
// Deleting the next highest version also allowed
365+
err = d.Actual.DeleteVersion(d.Superuser, secretName, 100)
366+
if err != nil {
367+
t.Fatalf("failed to delete next-highest version: %s", err)
368+
}
369+
checkActiveVersion(d, testValue2)
370+
371+
// Re-creating deleted version is not allowed
372+
err = d.Actual.CreateVersion(d.Superuser, secretName, 100, testValue2)
373+
if !errors.Is(err, db.ErrVersionTaken) {
374+
t.Fatalf("Recreating deleted version should have failed with %q but returned %q", db.ErrVersionTaken, err)
375+
}
376+
377+
// Putting gets the next higher version despite deletes
378+
version, err = d.Actual.Put(d.Superuser, secretName, testValue4)
379+
if err != nil {
380+
t.Fatalf("failed to Put new secret: %s", err)
381+
}
382+
if version != 102 {
383+
t.Fatalf("expected second Put to create version 102, but created %d", version)
384+
}
385+
checkVersion(d, 102, testValue4)
386+
checkActiveVersion(d, testValue2)
387+
})
388+
}
389+
230390
func TestDelete(t *testing.T) {
231391
d := setectest.NewDB(t, nil)
232392
id := d.Superuser

db/kv.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,12 @@ type secret struct {
9494
// ActiveVersion is the secret version that gets returned to
9595
// clients who don't ask for a specific version of the secret.
9696
ActiveVersion api.SecretVersion
97-
// LatestVersion is the latest version that has already been used
98-
// by a previous Put.
97+
// LatestVersion is the highest version that has already been used
98+
// by a previous Put or CreateVersion.
9999
LatestVersion api.SecretVersion
100+
// DeletedVersions tracks versions that were previously set but
101+
// have since been deleted. These are not permitted to be set again.
102+
DeletedVersions map[api.SecretVersion]bool
100103
}
101104

102105
// byteString is an alias for a string, but encodes to JSON as the conventional
@@ -354,6 +357,51 @@ func (kv *kv) put(name string, value []byte) (api.SecretVersion, error) {
354357
return s.LatestVersion, nil
355358
}
356359

360+
// createVersion creates the specified version of the secret called name with
361+
// the specified value. For a secret that does not yet exist, createVersion creates
362+
// the secret, sets the specified version to the given value and makes this the
363+
// secret's initial version. For a secret that already exists, createVersion
364+
// returns ErrVersionExists if the specified version ever had a value; otherwise,
365+
// createVersion sets the specified version to the given value and immediately
366+
// activates this version.
367+
func (kv *kv) createVersion(name string, version api.SecretVersion, value []byte) error {
368+
s := kv.secrets[name]
369+
if s == nil {
370+
kv.secrets[name] = &secret{
371+
LatestVersion: version,
372+
ActiveVersion: version,
373+
Versions: map[api.SecretVersion]byteString{
374+
version: byteString(value),
375+
},
376+
}
377+
if err := kv.save(); err != nil {
378+
delete(kv.secrets, name)
379+
return err
380+
}
381+
return nil
382+
}
383+
384+
_, hasVersion := s.Versions[version]
385+
hadVersion := s.DeletedVersions != nil && s.DeletedVersions[version]
386+
if hasVersion || hadVersion {
387+
return ErrVersionTaken
388+
}
389+
390+
bsValue := byteString(value)
391+
s.Versions[version] = bsValue
392+
priorLatestVersion := s.LatestVersion
393+
priorActiveVersion := s.ActiveVersion
394+
s.LatestVersion = max(priorLatestVersion, version)
395+
s.ActiveVersion = version
396+
if err := kv.save(); err != nil {
397+
delete(s.Versions, version)
398+
s.LatestVersion = priorLatestVersion
399+
s.ActiveVersion = priorActiveVersion
400+
return err
401+
}
402+
return nil
403+
}
404+
357405
// setActive changes the active version of the secret called name to
358406
// version.
359407
func (kv *kv) setActive(name string, version api.SecretVersion) error {
@@ -395,8 +443,17 @@ func (kv *kv) deleteVersion(name string, version api.SecretVersion) error {
395443
return fmt.Errorf("version %v: %w", version, ErrNotFound)
396444
}
397445
delete(secret.Versions, version)
446+
if secret.DeletedVersions == nil {
447+
secret.DeletedVersions = map[api.SecretVersion]bool{
448+
version: true,
449+
}
450+
} else {
451+
secret.DeletedVersions[version] = true
452+
}
453+
398454
if err := kv.save(); err != nil {
399455
secret.Versions[version] = old
456+
delete(secret.DeletedVersions, version)
400457
return err
401458
}
402459
return nil

docs/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ The [setec API](api.md) defines the following basic operations:
8080
- The `put` method creates or adds a new value to a secret. The server assigns
8181
and reports a version number for the value.
8282

83+
- The `create-version` method creates a specific version of a secret, sets its
84+
value and immediately activates that version. It fails if a value has ever
85+
been set for that version.
86+
87+
`put` and `create-version` are safe to use in conjunction with each other on the same
88+
secret.
89+
8390
### Current Active Versions
8491

8592
- At any time, one version of the secret is designated as its **current active
@@ -92,6 +99,8 @@ The [setec API](api.md) defines the following basic operations:
9299
- Thereafter, the `activate` method must be used to update the current active
93100
version. This ensures the operator of the service has precise control over
94101
which version of a secret should be used at a time.
102+
- The special `create-version` method automatically activates the set
103+
version and does not require a call to `activate`.
95104

96105
### Deleting Values
97106

0 commit comments

Comments
 (0)