Trust the vault's key alone and write the grants part's user list alone
The controller ended by its unassignment and its lease is never handed, so the engine's lease gate kept trusting the controller's key and the bus node kept the controller's list as a fallback nobody writes (issue 1126, A6). The engine now trusts no controller signature, its hand-over line names the node seat's hand-over, and the user-list follower writes only the grants part's list: a key unreadable or deleted keeps the list loaded, and with no key heard nothing is written. Compositions in the node's bucket name the file's place alone. The guard no longer asks for the controller's user. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -59,19 +59,6 @@ func (f *feed) Watch(context.Context, string) (<-chan agent.Update, error) {
|
||||
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")
|
||||
@@ -132,9 +119,8 @@ 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
|
||||
// unreadable and deleted make the key a value that does not read, or a deleted key, instead of a list (774/S31).
|
||||
unreadable, deleted bool
|
||||
}
|
||||
|
||||
type put struct {
|
||||
@@ -145,13 +131,12 @@ type put struct {
|
||||
}
|
||||
|
||||
// Defends novox/hq ADR 0547 §8: the bus node writes the newest signed list alone, only when it changed, and reports it.
|
||||
func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
one, two, three := list("controller", "a"), list("controller", "a", "b"), list("controller", "a", "b", "c")
|
||||
five := list("controller", "a", "b", "c", "d")
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
before, after []put
|
||||
again bool
|
||||
failWrite bool
|
||||
onDisk string
|
||||
wantWrites []string // contents written, in order
|
||||
@@ -166,83 +151,48 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
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},
|
||||
{name: "a newer revision is written, the older fingerprint is not",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: one}, {module: "nats", fp: fpOf("b"), content: two}},
|
||||
after: []put{{module: "nats", fp: fpOf("a"), content: three}},
|
||||
wantWrites: []string{two, three}, wantUsers: 4},
|
||||
{name: "a composition signed by a stranger is no source",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: one}},
|
||||
after: []put{{module: "nats", fp: fpOf("b"), content: three, stranger: true}},
|
||||
wantWrites: []string{one}, wantUsers: 2},
|
||||
{name: "a composition without the list is ignored", before: []put{{module: "web", fp: fpOf("a"), content: ""}}},
|
||||
{name: "a deleted key is no source",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: one}, {module: "nats", fp: fpOf("b"), content: two}},
|
||||
after: []put{{module: "nats", fp: fpOf("b"), content: "", deleted: true}, {module: "nats", fp: fpOf("a"), content: three}},
|
||||
wantWrites: []string{two, three}, wantUsers: 4},
|
||||
{name: "asked again, the same source is written again", before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
again: true, wantWrites: []string{two, two}, wantUsers: 3},
|
||||
{name: "a list sent by digest counts the users of what it names",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: three, byDigest: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4},
|
||||
{name: "another node's composition under this node's key is no source",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, asNode: "ace"}}},
|
||||
{name: "another module's composition under the bus module's key is no source",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, asModule: "web"}}},
|
||||
{name: "a list without the controller is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: list("a", "b")}},
|
||||
onDisk: two, wantUsers: 2, wantErr: "holds no user \"controller\"", wantRefused: true},
|
||||
{name: "a list that drops more than half is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: one}},
|
||||
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",
|
||||
// Issue 1126, A6 (ADR 0547 clauses 7, 8, 11): the grants part's list alone, written into the file the bus module's
|
||||
// composition declares; the list a composition carries is never written, whatever becomes of the grants key.
|
||||
{name: "the grants part's list is written into the file a composition declares",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a hand-back writes the newest controller composition once",
|
||||
{name: "a key that says follow the controller still writes the grants part's list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowController, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a newer composition's list is not written",
|
||||
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",
|
||||
after: []put{{module: "nats", fp: fpOf("b"), content: five}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a composition without the list is ignored", before: []put{{module: "web", fp: fpOf("a"), content: ""}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}}},
|
||||
{name: "a composition signed by a stranger declares no file",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, stranger: true}}, grants: []gput{{follow: delivery.FollowGrants, content: three}}},
|
||||
{name: "an unreadable grants key with none heard before writes nothing, never the composition's list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{unreadable: true}},
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
{name: "a refused watch with no key kept writes nothing 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",
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
{name: "a refused watch keeps the key last heard",
|
||||
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},
|
||||
// 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",
|
||||
{name: "a grants key that turns unreadable keeps the loaded 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},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a grants key that is deleted keeps the loaded list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}, {deleted: true, after: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
{name: "a grants list that drops more than half is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: one}},
|
||||
onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
{name: "a failed write is said and kept", before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}}, failWrite: true,
|
||||
wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: delivery.GrantsUserListSource},
|
||||
{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},
|
||||
@@ -293,7 +243,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), LeaseHanded: g.handed})
|
||||
Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -323,18 +273,6 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
},
|
||||
Key: func() string { return nodeKey.PublicKey },
|
||||
Trusted: func() []ed25519.PublicKey { return []ed25519.PublicKey{controllerPub} },
|
||||
Resolve: func(_ context.Context, r json.RawMessage) ([]byte, error) {
|
||||
var res struct{ Content, Digest string }
|
||||
_ = json.Unmarshal(r, &res)
|
||||
if res.Digest != "" {
|
||||
body, ok := objects[res.Digest]
|
||||
if !ok {
|
||||
return nil, errors.New("no object " + res.Digest)
|
||||
}
|
||||
return []byte(body), nil
|
||||
}
|
||||
return []byte(res.Content), nil
|
||||
},
|
||||
OnDisk: func(json.RawMessage) ([]byte, error) {
|
||||
if c.onDisk == "" {
|
||||
return nil, nil
|
||||
@@ -357,12 +295,6 @@ 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++
|
||||
@@ -411,14 +343,6 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
gf.ch <- u
|
||||
}
|
||||
}
|
||||
if c.again {
|
||||
// After the first write is reported, so the look again is not folded into it.
|
||||
select {
|
||||
case <-reported:
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
fl.Again()
|
||||
}
|
||||
deadline := time.After(5 * time.Second)
|
||||
for {
|
||||
mu.Lock()
|
||||
@@ -489,22 +413,6 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
|
||||
(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