diff --git a/internal/userlist/userlist.go b/internal/userlist/userlist.go index 536da9b..bfeaf3d 100644 --- a/internal/userlist/userlist.go +++ b/internal/userlist/userlist.go @@ -27,6 +27,11 @@ // against the key's digest, and written to the same file (mode 0600) only when the file's digest differs; otherwise the // controller's compositions are the source, as before. Never both. The have says which it follows and this node's key, // which the grants part seals to. +// +// **From the lease write it never follows the controller's list again** (ADR 0571 clause 8, ADR 0547 clause 11). The +// grants part's key then says the lease is handed (delivery.GrantsUserList.LeaseHanded): a key that later cannot be read or +// is deleted is said, the key last heard stands, and the list loaded stays; no list is written from a composition of the +// controller. package userlist import ( @@ -250,23 +255,36 @@ func (f *Follower) busNode() bool { } // takeGrants keeps the grants part's key as heard; a value it cannot read is said and leaves the controller's list -// followed (no list is followed that the node cannot check). +// followed (no list is followed that the node cannot check), unless the key last heard says the lease is handed: then +// the controller's list is never followed again (novox/hq ADR 0571 clause 8, ADR 0547 clause 11), the key as last heard +// stands, and the list loaded stays. func (f *Follower) takeGrants(u agent.Update) { f.mu.Lock() defer f.mu.Unlock() - f.grantsRev = u.Revision + handed := f.grants != nil && f.grants.LeaseHanded if u.Deleted { + if handed { + f.sayOnceLocked("the grants part's user-list key is deleted, and the lease is handed, so the list loaded is kept") + return + } + f.grantsRev = u.Revision f.grants = nil f.keep(nil, 0) return } g, err := delivery.DecodeGrantsUserList(u.Value) if err != nil { + if handed { + f.sayOnceLocked("the grants part's user-list key cannot be read, and the lease is handed, so the list loaded is kept: " + err.Error()) + return + } + f.grantsRev = u.Revision 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.grantsRev = u.Revision f.grants = &g f.keep(u.Value, u.Revision) } @@ -278,7 +296,8 @@ func (f *Follower) keep(value []byte, rev uint64) { } } -// Following is which list the follower takes now: the grants part's while its key says so, the controller's otherwise. +// Following is which list the follower takes now: the grants part's while its key says so or says the lease is handed, the +// controller's otherwise. func (f *Follower) Following() string { f.mu.Lock() defer f.mu.Unlock() @@ -286,7 +305,7 @@ func (f *Follower) Following() string { } func (f *Follower) followingLocked() string { - if f.grants != nil && f.grants.Follow == delivery.FollowGrants { + if f.grants != nil && (f.grants.Follow == delivery.FollowGrants || f.grants.LeaseHanded) { return delivery.FollowGrants } return delivery.FollowController diff --git a/internal/userlist/userlist_test.go b/internal/userlist/userlist_test.go index 94e89b3..e43240c 100644 --- a/internal/userlist/userlist_test.go +++ b/internal/userlist/userlist_test.go @@ -132,6 +132,9 @@ type gput struct { 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 + // handed says the controller's lease is handed (ADR 0582 clause 3); unreadable and deleted make the key a value that + // does not read, or a deleted key, instead of a list (774/S31). + handed, unreadable, deleted bool } type put struct { @@ -226,6 +229,20 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) { {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}, + // 774/S31 (ADR 0571 clause 8, ADR 0547 clause 11): once the key says the lease is handed, the controller's list is never + // followed again, whatever becomes of the key; without the flag the controller's list is still the fallback. + {name: "a handed lease: a grants key that turns unreadable keeps the loaded list and never writes the controller's", + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, + grants: []gput{{follow: delivery.FollowGrants, content: three, handed: true}, {unreadable: true, after: true}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + {name: "a handed lease: a grants key that is deleted keeps the loaded list and never writes the controller's", + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, + grants: []gput{{follow: delivery.FollowGrants, content: three, handed: true}, {deleted: true, after: true}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + {name: "no handed lease: a grants key that turns unreadable falls back to the controller's list", + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, + grants: []gput{{follow: delivery.FollowGrants, content: three}, {unreadable: true, after: true}}, + wantWrites: []string{three, 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, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource}, @@ -256,6 +273,11 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) { gs = append(append([]gput(nil), gs...), *c.kept) } for i, g := range gs { + if g.unreadable || g.deleted { + u := agent.Update{Key: delivery.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted} + grantsAfter = append(grantsAfter, u) + continue + } to := nodeKey.PublicKey if g.otherKey { other, _ := seal.GenerateKeyPair() @@ -271,7 +293,7 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) { } sealed[delivery.UserListObjectName(digest)] = object raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest, - Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)}) + Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1), LeaseHanded: g.handed}) if err != nil { t.Fatal(err) }