Bring in the adoption handing; the bus node writes nothing before it heard the switch, retries the key's watch and keeps the follow last heard, and an apply never writes the controller's list while the grants part's is followed (review of the switch, issue 1945)
Merges fix/1945-adopt-hashes 1a190090. A new follower (one per link) waits for the grants part's key to catch up before
its first write, so a restart after take-over never writes the controller's list first. A refused or ended watch is
tried again on link.Backoff, the have says why it is not watched, and the key last heard, kept beside the node's state,
holds meanwhile. While the grants part's list is followed, the bus module's composition is applied with the list on the
machine in place of the controller's. The grants list is never handed to the vault. Pins mesh-sdk c2a410bb.
This commit is contained in:
+177
-23
@@ -16,6 +16,11 @@
|
||||
//
|
||||
// It reads no want and writes none, moves no fingerprint, and touches nothing of the bus module but its user list.
|
||||
//
|
||||
// **It hands the loaded list's hashes to the vault** (ADR 0547 clause 9, issue 1945): once the vault's key is known
|
||||
// (VaultKey), the have of the bus module carries the bcrypt hash of every user of the list on the machine, sealed to
|
||||
// that key (delivery.AccountsHanded), so the vault adopts the accounts it holds no value for. It is sealed again only
|
||||
// when the list on the machine or the vault's key changes.
|
||||
//
|
||||
// **From take-over it follows the grants part's list instead** (ADR 0547 clauses 7 and 8, issue 1945). On the node whose
|
||||
// compositions carry the list, it also watches the grants part's one key `user-list.sealed`. While that key says
|
||||
// FollowGrants, the list is the object the key names, read by its name, opened with this node's sealing key and checked
|
||||
@@ -74,8 +79,17 @@ type Env struct {
|
||||
Fetch func(ctx context.Context, name string) ([]byte, error)
|
||||
Open func(seal.Box) ([]byte, error)
|
||||
Key func() string
|
||||
Now func() time.Time
|
||||
Say func(string)
|
||||
// Kept is the grants part's key as last heard on this machine and its revision (nil when none), and Keep keeps it:
|
||||
// what the follower follows while the key cannot be watched (ADR 0547 clause 8). Backoff is the wait before the
|
||||
// try-th watch again.
|
||||
Kept func() ([]byte, uint64)
|
||||
Keep func(value []byte, rev uint64)
|
||||
Backoff func(try int) time.Duration
|
||||
// VaultKey is the vault's own public key (SPKI PEM) as its answer to node-key gave it, "" while none is known: the
|
||||
// key the loaded list's hashes are handed sealed to. Nil hands nothing.
|
||||
VaultKey func() string
|
||||
Now func() time.Time
|
||||
Say func(string)
|
||||
}
|
||||
|
||||
type source struct {
|
||||
@@ -103,6 +117,8 @@ type Follower struct {
|
||||
grants *delivery.GrantsUserList
|
||||
grantsRev uint64
|
||||
following string
|
||||
// unwatched is why the grants part's key is not watched now; empty while it is, or where it need not be.
|
||||
unwatched string
|
||||
}
|
||||
|
||||
// New is a follower for a node.
|
||||
@@ -113,7 +129,18 @@ func New(env Env) *Follower {
|
||||
if env.Say == nil {
|
||||
env.Say = func(string) {}
|
||||
}
|
||||
return &Follower{env: env, kick: make(chan struct{}, 1), srcs: map[string]source{}, said: map[string]bool{}}
|
||||
if env.Backoff == nil {
|
||||
env.Backoff = func(int) time.Duration { return time.Minute }
|
||||
}
|
||||
f := &Follower{env: env, kick: make(chan struct{}, 1), srcs: map[string]source{}, said: map[string]bool{}}
|
||||
if env.Grants != nil && env.Kept != nil {
|
||||
if raw, rev := env.Kept(); raw != nil {
|
||||
if g, err := delivery.DecodeGrantsUserList(raw); err == nil {
|
||||
f.grants, f.grantsRev = &g, rev
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Again asks the follower to look at the file again, though no composition changed: after an apply of the bus module,
|
||||
@@ -140,7 +167,33 @@ func (f *Follower) Run(ctx context.Context) error {
|
||||
return fmt.Errorf("the user list cannot be followed: %w", err)
|
||||
}
|
||||
caught := false
|
||||
// The grants part's key, on the node whose compositions carry the list: the bus node alone is granted it (mesh-sdk
|
||||
// buscompose.GrantsUserListFollows). Until its watch caught up nothing is written, so a new follower (one per link)
|
||||
// never writes the controller's list after take-over before it heard the switch; a watch refused or ended is tried
|
||||
// again on the backoff, and meanwhile the follow last heard (kept on the machine) holds.
|
||||
var grants <-chan agent.Update
|
||||
var retry <-chan time.Time
|
||||
opened, grantsCaught, tries := false, false, 0
|
||||
failed := func(why string) {
|
||||
grants, tries = nil, tries+1
|
||||
retry = time.After(f.env.Backoff(tries))
|
||||
f.mu.Lock()
|
||||
f.unwatched = why
|
||||
f.mu.Unlock()
|
||||
f.sayOnce("the grants part's user-list key is not watched, tried again: " + why)
|
||||
}
|
||||
open := func() {
|
||||
if grants != nil || retry != nil || f.env.Grants == nil || !f.busNode() {
|
||||
return
|
||||
}
|
||||
opened = true
|
||||
ch, err := f.env.Grants.Watch(ctx, delivery.GrantsUserListKey)
|
||||
if err != nil {
|
||||
failed(err.Error())
|
||||
return
|
||||
}
|
||||
grants = ch
|
||||
}
|
||||
for {
|
||||
again := false
|
||||
select {
|
||||
@@ -148,6 +201,8 @@ func (f *Follower) Run(ctx context.Context) error {
|
||||
return nil
|
||||
case <-f.kick:
|
||||
again = true
|
||||
case <-retry:
|
||||
retry = nil
|
||||
case u, ok := <-updates:
|
||||
if !ok {
|
||||
return fmt.Errorf("the watch of the compositions ended")
|
||||
@@ -159,29 +214,31 @@ func (f *Follower) Run(ctx context.Context) error {
|
||||
}
|
||||
case u, ok := <-grants:
|
||||
if !ok {
|
||||
// Its watch ended (the link, or a grant the bus took away): heard again at the next event.
|
||||
grants = nil
|
||||
f.sayOnce("the grants part's user list is not followed until its key can be watched again")
|
||||
continue
|
||||
failed("its watch ended")
|
||||
break
|
||||
}
|
||||
if u.Caught {
|
||||
continue // nothing changed by the watch catching up
|
||||
grantsCaught, tries = true, 0
|
||||
f.mu.Lock()
|
||||
f.unwatched = ""
|
||||
f.mu.Unlock()
|
||||
break
|
||||
}
|
||||
f.takeGrants(u)
|
||||
}
|
||||
if caught {
|
||||
f.settle(ctx, again)
|
||||
// The grants part's key is watched on the node whose compositions carry the list: the bus node alone is
|
||||
// granted it (mesh-sdk buscompose.GrantsUserListFollows).
|
||||
if grants == nil && f.env.Grants != nil && f.busNode() {
|
||||
ch, err := f.env.Grants.Watch(ctx, delivery.GrantsUserListKey)
|
||||
if err != nil {
|
||||
f.sayOnce("the grants part's user list cannot be watched yet, so the controller's is followed: " + err.Error())
|
||||
} else {
|
||||
grants = ch
|
||||
}
|
||||
}
|
||||
if !caught {
|
||||
continue
|
||||
}
|
||||
open()
|
||||
if opened && !grantsCaught && grants != nil {
|
||||
continue // the switch is not heard yet: nothing is written before it is
|
||||
}
|
||||
f.mu.Lock()
|
||||
if f.unwatched != f.state.Unwatched {
|
||||
again = true // the have says whether the key is watched
|
||||
}
|
||||
f.mu.Unlock()
|
||||
f.settle(ctx, again)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,15 +257,25 @@ func (f *Follower) takeGrants(u agent.Update) {
|
||||
f.grantsRev = u.Revision
|
||||
if u.Deleted {
|
||||
f.grants = nil
|
||||
f.keep(nil, 0)
|
||||
return
|
||||
}
|
||||
g, err := delivery.DecodeGrantsUserList(u.Value)
|
||||
if err != nil {
|
||||
f.grants = nil
|
||||
f.keep(nil, 0)
|
||||
f.sayOnceLocked("the grants part's user-list key cannot be read, so the controller's list is followed: " + err.Error())
|
||||
return
|
||||
}
|
||||
f.grants = &g
|
||||
f.keep(u.Value, u.Revision)
|
||||
}
|
||||
|
||||
// keep keeps the key last heard on the machine (Env.Keep), so a follower that cannot watch it follows what it said.
|
||||
func (f *Follower) keep(value []byte, rev uint64) {
|
||||
if f.env.Keep != nil {
|
||||
f.env.Keep(value, rev)
|
||||
}
|
||||
}
|
||||
|
||||
// Following is which list the follower takes now: the grants part's while its key says so, the controller's otherwise.
|
||||
@@ -225,6 +292,12 @@ func (f *Follower) followingLocked() string {
|
||||
return delivery.FollowController
|
||||
}
|
||||
|
||||
func (f *Follower) unwatchedNow() string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.unwatched
|
||||
}
|
||||
|
||||
func (f *Follower) key() string {
|
||||
if f.env.Key == nil {
|
||||
return ""
|
||||
@@ -308,7 +381,7 @@ func (f *Follower) settle(ctx context.Context, again bool) {
|
||||
f.mu.Unlock()
|
||||
|
||||
w := Written{Source: newest.key, Revision: newest.rev, Wanted: newest.wanted, At: f.env.Now().UTC(),
|
||||
Follows: delivery.FollowController, Key: f.key()}
|
||||
Follows: delivery.FollowController, Key: f.key(), Unwatched: f.unwatchedNow()}
|
||||
var changed bool
|
||||
body, err := f.env.Resolve(ctx, newest.resource)
|
||||
if err == nil {
|
||||
@@ -321,14 +394,24 @@ func (f *Follower) settle(ctx context.Context, again bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
var loaded []byte
|
||||
if err == nil {
|
||||
w.Digest, changed, err = f.env.Write(ctx, newest.module, newest.resource, body)
|
||||
if err == nil {
|
||||
loaded = body
|
||||
}
|
||||
} else if body != nil && f.env.OnDisk != nil {
|
||||
// Refused: the list on the machine stays, and it is the one the bus has loaded.
|
||||
loaded, _ = f.env.OnDisk(newest.resource)
|
||||
}
|
||||
if err != nil {
|
||||
w.Error = err.Error()
|
||||
}
|
||||
f.mu.Lock()
|
||||
prev := f.state
|
||||
f.mu.Unlock()
|
||||
w.Handed = f.hand(loaded, prev.Handed)
|
||||
f.mu.Lock()
|
||||
f.mod, f.state = newest.module, w
|
||||
f.written = err == nil
|
||||
f.following = delivery.FollowController
|
||||
@@ -348,7 +431,8 @@ func (f *Follower) settle(ctx context.Context, again bool) {
|
||||
// reportable says a have's user list says something new: a look again that found the file as it was says nothing.
|
||||
func reportable(prev, w Written) bool {
|
||||
return prev.Source != w.Source || prev.Revision != w.Revision || prev.Digest != w.Digest || prev.Error != w.Error ||
|
||||
prev.Follows != w.Follows || prev.Key != w.Key
|
||||
prev.Follows != w.Follows || prev.Key != w.Key || handedKey(prev.Handed) != handedKey(w.Handed) ||
|
||||
prev.Unwatched != w.Unwatched
|
||||
}
|
||||
|
||||
// settleGrants writes the grants part's list, the object its key names, into the file the controller's composition of
|
||||
@@ -369,7 +453,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
f.mu.Unlock()
|
||||
|
||||
w := Written{Source: delivery.GrantsUserListSource, Revision: rev, Wanted: g.Digest, At: f.env.Now().UTC(),
|
||||
Follows: delivery.FollowGrants, Key: f.key()}
|
||||
Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()}
|
||||
var err error
|
||||
var changed bool
|
||||
resource, rerr := mode0600(file.resource)
|
||||
@@ -413,6 +497,8 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
}
|
||||
f.mu.Lock()
|
||||
prev := f.state
|
||||
// The grants part's list is not handed to the vault: the handing is of what the controller's list loaded (clause 9).
|
||||
w.Handed = prev.Handed
|
||||
f.mod, f.state = file.module, w
|
||||
f.written = err == nil
|
||||
f.following = delivery.FollowGrants
|
||||
@@ -429,6 +515,38 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Hold is c as the apply may write it (C3 of the switch's review): while the grants part's list is followed, the bus
|
||||
// module's composition from the controller carries the controller's list, so its user-list resource holds the list on the
|
||||
// machine instead (made 0600), or is left out when none can be read. Any other composition, or the controller's list
|
||||
// followed, is c unchanged.
|
||||
func (f *Follower) Hold(c delivery.Composed) delivery.Composed {
|
||||
if f.Following() != delivery.FollowGrants {
|
||||
return c
|
||||
}
|
||||
out := c
|
||||
out.Resources = nil
|
||||
for _, raw := range c.Resources {
|
||||
if r, _, _, ok := userList([]json.RawMessage{raw}); ok {
|
||||
var held json.RawMessage
|
||||
if body, err := f.env.OnDisk(r); err == nil && body != nil {
|
||||
var m map[string]any
|
||||
if json.Unmarshal(r, &m) == nil {
|
||||
delete(m, "digest")
|
||||
delete(m, "size")
|
||||
m["content"], m["mode"] = string(body), "0600"
|
||||
held, _ = json.Marshal(m)
|
||||
}
|
||||
}
|
||||
if held != nil {
|
||||
out.Resources = append(out.Resources, held)
|
||||
}
|
||||
continue
|
||||
}
|
||||
out.Resources = append(out.Resources, raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mode0600 is a user-list resource made mode 0600, whatever its composition said (ADR 0547 clause 7).
|
||||
func mode0600(raw json.RawMessage) (json.RawMessage, error) {
|
||||
if len(raw) == 0 {
|
||||
@@ -442,6 +560,42 @@ func mode0600(raw json.RawMessage) (json.RawMessage, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// hand is the handing of the list on the machine to the vault: the one before when it is of the same list and key, a
|
||||
// new one sealed now otherwise, and none while the vault's key is not known or the list holds no hash.
|
||||
func (f *Follower) hand(loaded []byte, before *delivery.AccountsHanded) *delivery.AccountsHanded {
|
||||
if f.env.VaultKey == nil || loaded == nil {
|
||||
return before
|
||||
}
|
||||
key := f.env.VaultKey()
|
||||
if key == "" {
|
||||
return before
|
||||
}
|
||||
from := Digest(loaded)
|
||||
if before != nil && before.From == from && before.To == delivery.KeyFingerprint(key) {
|
||||
return before
|
||||
}
|
||||
accounts := delivery.HandedAccountsOf(loaded)
|
||||
if len(accounts) == 0 {
|
||||
return nil
|
||||
}
|
||||
h, err := delivery.HandAccounts(accounts, key, from, f.env.Now())
|
||||
if err != nil {
|
||||
f.sayOnce(fmt.Sprintf("the loaded list's hashes are not handed to the vault: %v", err))
|
||||
return before
|
||||
}
|
||||
f.env.Say(fmt.Sprintf("the hashes of the %d users of the bus's loaded list (%s) are handed to the vault, sealed to "+
|
||||
"its key %s, in the bus module's have", h.Users, short(from), short(h.To)))
|
||||
return &h
|
||||
}
|
||||
|
||||
// handedKey is what a handing says that a report compares: its list and key.
|
||||
func handedKey(h *delivery.AccountsHanded) string {
|
||||
if h == nil {
|
||||
return ""
|
||||
}
|
||||
return h.From + " " + h.To
|
||||
}
|
||||
|
||||
// ControllerUser is the controller's own user in the list: a list without it would lock the controller out of the bus.
|
||||
const ControllerUser = "controller"
|
||||
|
||||
|
||||
@@ -30,29 +30,48 @@ var _, strangerKey = key()
|
||||
|
||||
func fpOf(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) }
|
||||
|
||||
// feed is a watch the test writes to: the keys there before the watch, Caught, then each put.
|
||||
// feed is a watch the test writes to: the keys there before the watch, Caught, then each put. watching is closed once
|
||||
// the watch began, so a put after it is never handed before the keys there before it.
|
||||
type feed struct {
|
||||
ch chan agent.Update
|
||||
rev uint64
|
||||
first []agent.Update
|
||||
// watched is closed once the watch caught up, so a later put is not read before the keys there before it.
|
||||
watched chan struct{}
|
||||
once sync.Once
|
||||
// watching is closed once the watch caught up, so a later put is not read before the keys there before it.
|
||||
watching chan struct{}
|
||||
once sync.Once
|
||||
// err refuses the watch, as the bus refuses a watch it grants nothing of.
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *feed) Watch(context.Context, string) (<-chan agent.Update, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
for _, u := range f.first {
|
||||
f.ch <- u
|
||||
}
|
||||
f.ch <- agent.Update{Caught: true}
|
||||
f.once.Do(func() {
|
||||
if f.watched != nil {
|
||||
close(f.watched)
|
||||
if f.watching != nil {
|
||||
close(f.watching)
|
||||
}
|
||||
})
|
||||
return f.ch, nil
|
||||
}
|
||||
|
||||
// hashed is a list whose users carry a hash of bcrypt's form, as the composer writes them.
|
||||
func hashed(users ...string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("authorization {\n users = [\n")
|
||||
for _, u := range users {
|
||||
b.WriteString(` { user: "` + u + `", password: "$2a$11$` + strings.Repeat("h", 53) + `", permissions: {` + "\n")
|
||||
}
|
||||
b.WriteString(" ]\n}\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var vault, _ = seal.GenerateKeyPair()
|
||||
|
||||
func list(users ...string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("authorization {\n users = [\n")
|
||||
@@ -111,6 +130,8 @@ func comp(t *testing.T, module, fp, content string, signer ed25519.PrivateKey, p
|
||||
type gput struct {
|
||||
follow, content string
|
||||
otherKey, badDigest bool
|
||||
// after puts it once the grants part's list was written, as a hand-back comes after a take-over.
|
||||
after bool
|
||||
}
|
||||
|
||||
type put struct {
|
||||
@@ -135,8 +156,17 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
wantErr string
|
||||
wantRefused bool
|
||||
grants []gput
|
||||
// grantsRefused refuses the watch of the grants part's key; kept is the key last heard, kept on the machine;
|
||||
// wantUnwatched says the have then says why it is not watched.
|
||||
grantsRefused bool
|
||||
kept *gput
|
||||
wantUnwatched bool
|
||||
// wantSource and wantFollows are what the last have says it took the list from and follows.
|
||||
wantSource, wantFollows string
|
||||
// vault gives the follower the vault's key; wantHanded is how many users it then hands from the list on the
|
||||
// machine (ADR 0547 clause 9).
|
||||
vault bool
|
||||
wantHanded int
|
||||
}{
|
||||
{name: "the list there is written once", before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
wantWrites: []string{two}, wantUsers: 3},
|
||||
@@ -170,36 +200,62 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true},
|
||||
{name: "a list that drops half is written", before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
onDisk: list("controller", "a", "b", "c", "d", "e"), wantWrites: []string{two}, wantUsers: 3},
|
||||
{name: "the loaded list's hashes are handed sealed to the vault's key",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: hashed("controller", "node.ace")}}, vault: true,
|
||||
wantWrites: []string{hashed("controller", "node.ace")}, wantUsers: 2, wantHanded: 2},
|
||||
{name: "a refused list hands the list on the machine",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: list("a", "b")}}, vault: true,
|
||||
onDisk: hashed("controller", "a", "b"), wantUsers: 2, wantErr: "holds no user", wantRefused: true, wantHanded: 3},
|
||||
{name: "a failed write is said and kept", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, failWrite: true,
|
||||
wantWrites: []string{two}, wantUsers: 3, wantErr: "disk full"},
|
||||
// ADR 0547 clauses 7, 8: never both lists; the grants part's from take-over, the controller's otherwise.
|
||||
{name: "from take-over the grants part's list is written into the same file",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{two, three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a hand-back writes the newest controller composition once",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}, {follow: delivery.FollowController, content: three, after: true}},
|
||||
wantWrites: []string{three, two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController},
|
||||
{name: "a refused watch with no key kept follows the controller's list and says it is not watched",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true,
|
||||
wantWrites: []string{two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController},
|
||||
{name: "a refused watch keeps the follow last heard, never the controller's list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true,
|
||||
kept: &gput{follow: delivery.FollowGrants, content: three},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "in shadow the grants part's list is not taken",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowController, content: three}},
|
||||
wantWrites: []string{two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController},
|
||||
{name: "a grants list that does not open to its digest is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, badDigest: true}},
|
||||
onDisk: two, wantWrites: []string{two}, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
{name: "a grants list sealed to another key is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, otherKey: true}},
|
||||
onDisk: two, wantWrites: []string{two}, wantErr: "sealed to another key", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
f := &feed{ch: make(chan agent.Update, 64), watched: make(chan struct{})}
|
||||
f := &feed{ch: make(chan agent.Update, 64), watching: make(chan struct{})}
|
||||
objects := map[string]string{}
|
||||
var mu sync.Mutex
|
||||
var writes []string
|
||||
var reports []Written
|
||||
reported := make(chan struct{}, 64)
|
||||
gf := &feed{ch: make(chan agent.Update, 64)}
|
||||
if c.grantsRefused {
|
||||
gf.err = errors.New("Permissions Violation for Publish to $JS.API.STREAM.INFO.KV_mesh-grants_user-list")
|
||||
}
|
||||
var grantsAfter []agent.Update
|
||||
var keptRaw []byte
|
||||
sealed := map[string][]byte{}
|
||||
nodeKey, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, g := range c.grants {
|
||||
gs := c.grants
|
||||
if c.kept != nil {
|
||||
gs = append(append([]gput(nil), gs...), *c.kept)
|
||||
}
|
||||
for i, g := range gs {
|
||||
to := nodeKey.PublicKey
|
||||
if g.otherKey {
|
||||
other, _ := seal.GenerateKeyPair()
|
||||
@@ -219,9 +275,20 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gf.first = append(gf.first, agent.Update{Key: delivery.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)})
|
||||
u := agent.Update{Key: delivery.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)}
|
||||
switch {
|
||||
case c.kept != nil && i == len(gs)-1:
|
||||
keptRaw = raw
|
||||
case g.after:
|
||||
grantsAfter = append(grantsAfter, u)
|
||||
default:
|
||||
gf.first = append(gf.first, u)
|
||||
}
|
||||
}
|
||||
fl := New(Env{Node: node, Composed: f, Grants: gf,
|
||||
Kept: func() ([]byte, uint64) { return keptRaw, 9 },
|
||||
Keep: func([]byte, uint64) {},
|
||||
Backoff: func(int) time.Duration { return time.Hour },
|
||||
Fetch: func(_ context.Context, name string) ([]byte, error) {
|
||||
if o, ok := sealed[name]; ok {
|
||||
return o, nil
|
||||
@@ -268,6 +335,12 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
reported <- struct{}{}
|
||||
},
|
||||
Now: func() time.Time { return time.Date(2026, 10, 25, 21, 50, 0, 0, time.UTC) },
|
||||
VaultKey: func() string {
|
||||
if c.vault {
|
||||
return vault.PublicKey
|
||||
}
|
||||
return ""
|
||||
},
|
||||
})
|
||||
update := func(p put) agent.Update {
|
||||
f.rev++
|
||||
@@ -291,10 +364,31 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- fl.Run(ctx) }()
|
||||
<-f.watched
|
||||
<-f.watching
|
||||
// A later put after the first list is written: the first write waits for the grants part's key to catch up.
|
||||
if len(c.after) > 0 {
|
||||
for until := time.Now().Add(5 * time.Second); time.Now().Before(until); time.Sleep(5 * time.Millisecond) {
|
||||
mu.Lock()
|
||||
n := len(writes)
|
||||
mu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range c.after {
|
||||
f.ch <- update(p)
|
||||
}
|
||||
if len(grantsAfter) > 0 {
|
||||
for until := time.Now().Add(5 * time.Second); ; time.Sleep(5 * time.Millisecond) {
|
||||
if _, st := fl.State(); st.Source == delivery.GrantsUserListSource || time.Now().After(until) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, u := range grantsAfter {
|
||||
gf.ch <- u
|
||||
}
|
||||
}
|
||||
if c.again {
|
||||
// After the first write is reported, so the look again is not folded into it.
|
||||
select {
|
||||
@@ -341,10 +435,41 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
t.Fatalf("written %q, want %q", writes, c.wantWrites)
|
||||
}
|
||||
module, st := fl.State()
|
||||
if c.wantUnwatched != (st.Unwatched != "") {
|
||||
t.Fatalf("the have says unwatched %q", st.Unwatched)
|
||||
}
|
||||
// While the grants part's list is followed, an apply of the controller's composition writes not its list.
|
||||
if _, raw := comp(t, "nats", fpOf("a"), two, controllerKey, put{}); c.wantFollows != "" {
|
||||
cmp, err := delivery.DecodeComposed(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held := fl.Hold(cmp)
|
||||
_, _, _, hasList := userList(held.Resources)
|
||||
if want := c.wantFollows == delivery.FollowController; hasList != want || len(held.Resources) < 1 {
|
||||
t.Fatalf("an apply while following %s holds the controller's list: %v", c.wantFollows, hasList)
|
||||
}
|
||||
}
|
||||
if c.wantSource != "" && (st.Source != c.wantSource || st.Key != nodeKey.PublicKey ||
|
||||
(c.wantFollows != "" && st.Follows != c.wantFollows)) {
|
||||
t.Fatalf("the have says source %q follows %q key %t; want %q %q", st.Source, st.Follows, st.Key == nodeKey.PublicKey, c.wantSource, c.wantFollows)
|
||||
}
|
||||
switch {
|
||||
case c.wantHanded == 0 && st.Handed != nil:
|
||||
t.Fatalf("handed with no vault key or no hash: %+v", st.Handed)
|
||||
case c.wantHanded > 0:
|
||||
loaded := c.onDisk
|
||||
if !c.wantRefused {
|
||||
loaded = c.wantWrites[len(c.wantWrites)-1]
|
||||
}
|
||||
if st.Handed == nil || st.Handed.From != Digest([]byte(loaded)) {
|
||||
t.Fatalf("handed %+v, want from the list on the machine", st.Handed)
|
||||
}
|
||||
opened, err := delivery.OpenHanded(*st.Handed, vault.PublicKey, vault.PrivateKey)
|
||||
if err != nil || len(opened) != c.wantHanded || opened[0].User != "controller" {
|
||||
t.Fatalf("opened %+v, %v", opened, err)
|
||||
}
|
||||
}
|
||||
if c.wantRefused {
|
||||
if !strings.Contains(st.Error, c.wantErr) || st.Digest != Digest([]byte(c.onDisk)) || st.Users != c.wantUsers {
|
||||
t.Fatalf("refused list's state %+v", st)
|
||||
|
||||
Reference in New Issue
Block a user