From d5eeb860e3dff0e778305d4baab979c9b0d8da64 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 15:41:09 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Read=20a=20file's=20object=20from=20the=20n?= =?UTF-8?q?ode's=20own=20bucket,=20declaration-files-,=20never=20a?= =?UTF-8?q?=20shared=20one=20(issue=201695,=20ADR=200486=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-sdk is pinned to fix/1694-bucket-per-node 36d9eba51db9 until it merges. --- go.mod | 2 +- go.sum | 2 ++ internal/apply/references.go | 4 ++-- internal/apply/references_test.go | 2 +- internal/link/bus.go | 2 ++ internal/link/files.go | 23 +++++++++++++-------- internal/link/files_nats_test.go | 33 ++++++++++++++++++++++++++----- internal/link/files_node_test.go | 17 ++++++++++++++++ 8 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 internal/link/files_node_test.go diff --git a/go.mod b/go.mod index f3cfcb0..cb2d687 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 golang.org/x/sys v0.48.0 diff --git a/go.sum b/go.sum index b97a407..0cc0e88 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f h1:7J/9jOfS git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e h1:eq29ENikw3XfEXjxL5OdnTbavv5pu1/UnQhWWuf5V5I= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9 h1:FSepuLYomAwsIsPTUhSs31kFwCJ+XIXfNUIcGYPwcsI= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/internal/apply/references.go b/internal/apply/references.go index b5b5ca2..ad6bed5 100644 --- a/internal/apply/references.go +++ b/internal/apply/references.go @@ -68,8 +68,8 @@ func referencedContent(ctx context.Context, f *declaration.File, fetch FetchObje } body, err := fetch(ctx, name, f.Size) if err != nil { - return nil, fmt.Errorf("file %s (%s): fetching the object %s of the bucket %s: %w", - f.ID, f.Path, name, declaration.FileBucket, err) + return nil, fmt.Errorf("file %s (%s): fetching the object %s of this node's own object bucket: %w", + f.ID, f.Path, name, err) } if err := f.Verify(body); err != nil { return nil, fmt.Errorf("%s (%s): %w; nothing was applied", f.ID, f.Path, err) diff --git a/internal/apply/references_test.go b/internal/apply/references_test.go index e02ffac..ec3f841 100644 --- a/internal/apply/references_test.go +++ b/internal/apply/references_test.go @@ -76,7 +76,7 @@ func TestAMissingObjectFailsTheApply(t *testing.T) { d := &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, "/nowhere/big", body)}} var asked []string if err := ResolveFileReferences(context.Background(), d, objects(nil, &asked)); err == nil || - !strings.Contains(err.Error(), "m.big") || !strings.Contains(err.Error(), "declaration-files") { + !strings.Contains(err.Error(), "m.big") || !strings.Contains(err.Error(), "object bucket") { t.Errorf("1695/AC3: missing object: %v", err) } if err := ResolveFileReferences(context.Background(), d, nil); err == nil || !strings.Contains(err.Error(), "no link to the bus") { diff --git a/internal/link/bus.go b/internal/link/bus.go index 62bd227..dda5a4d 100644 --- a/internal/link/bus.go +++ b/internal/link/bus.go @@ -35,6 +35,8 @@ type Bus interface { type OverNATS struct { Conn *nats.Conn JS nats.JetStreamContext + // Node is this node's name, which names its own object bucket (FileObject). Empty where nothing reads a file. + Node string } // ReportSubject is this node's own, and no other node's: a host's account may diff --git a/internal/link/files.go b/internal/link/files.go index 8ba885a..91e6959 100644 --- a/internal/link/files.go +++ b/internal/link/files.go @@ -49,12 +49,12 @@ type objectInfo struct { Deleted bool `json:"deleted"` } -// FileObject reads one object by name from the declaration-files bucket, without listing the bucket or looking at +// FileObject reads one object by name from this node's own object bucket (declaration-files-), without listing the bucket or looking at // anything but the object it names (novox/hq ADR 0486 ยง2: a node-engine's account may read by name and may neither // list nor write). // // **The bus subjects this uses, and no others** (the grant of issue 1697 is exactly these; reviewer's note 5 there), for -// bucket B = declaration-files, stream S = OBJ_B, and E the base64 (URL alphabet) of the object's name: +// bucket B = declaration-files-, stream S = OBJ_B, and E the base64 (URL alphabet) of the object's name: // // request $JS.API.DIRECT.GET.S.$O.B.M.E the object's metadata. Grant only as ...DIRECT.GET.S.$O.B.M.*, never // the bare ...DIRECT.GET.S, which takes a sequence number @@ -85,12 +85,19 @@ func (b OverNATS) FileObject(ctx context.Context, name string, limit int64) ([]b } js = bounded } - stream := "OBJ_" + declaration.FileBucket - meta, err := js.GetLastMsg(stream, "$O."+declaration.FileBucket+".M."+base64.URLEncoding.EncodeToString([]byte(name)), + if b.Node == "" { + return nil, errors.New("this link does not know its node's name, so it cannot name the node's own bucket") + } + bucket, err := declaration.FileBucketFor(b.Node) + if err != nil { + return nil, err + } + stream := declaration.FileStreamPrefix + bucket + meta, err := js.GetLastMsg(stream, "$O."+bucket+".M."+base64.URLEncoding.EncodeToString([]byte(name)), nats.DirectGet(), nats.Context(ctx)) if err != nil { if errors.Is(err, nats.ErrMsgNotFound) { - return nil, fmt.Errorf("no object %s in the bucket %s", name, declaration.FileBucket) + return nil, fmt.Errorf("no object %s in the bucket %s", name, bucket) } return nil, fmt.Errorf("the metadata of object %s: %w", name, err) } @@ -99,7 +106,7 @@ func (b OverNATS) FileObject(ctx context.Context, name string, limit int64) ([]b return nil, fmt.Errorf("the metadata of object %s is not readable: %w", name, err) } if info.Deleted { - return nil, fmt.Errorf("the object %s was deleted from the bucket %s", name, declaration.FileBucket) + return nil, fmt.Errorf("the object %s was deleted from the bucket %s", name, bucket) } if info.Size > uint64(limit) { return nil, fmt.Errorf("the object is larger than the %d bytes the declaration names", limit) @@ -107,7 +114,7 @@ func (b OverNATS) FileObject(ctx context.Context, name string, limit int64) ([]b if info.Size == 0 { return []byte{}, nil } - sub, err := js.SubscribeSync("$O."+declaration.FileBucket+".C."+info.NUID, + sub, err := js.SubscribeSync("$O."+bucket+".C."+info.NUID, nats.OrderedConsumer(), nats.BindStream(stream)) if err != nil { return nil, fmt.Errorf("reading the chunks of object %s: %w", name, err) @@ -132,7 +139,7 @@ func (b OverNATS) FileObject(ctx context.Context, name string, limit int64) ([]b } func (l *natsLink) FileObject(ctx context.Context, name string, limit int64) ([]byte, error) { - return OverNATS{Conn: l.conn, JS: l.js}.FileObject(ctx, name, limit) + return OverNATS{Conn: l.conn, JS: l.js, Node: l.node}.FileObject(ctx, name, limit) } var ( diff --git a/internal/link/files_nats_test.go b/internal/link/files_nats_test.go index dcaa09b..f55ce2c 100644 --- a/internal/link/files_nats_test.go +++ b/internal/link/files_nats_test.go @@ -11,8 +11,8 @@ import ( "github.com/nats-io/nats.go" ) -// 1695/AC7: (untagged on purpose: the merge check of the trunk runs the tagless suite against its throwaway bus, and the -// test skips without MESH_TEST_NATS) against a real bus with the bucket declaration-files, over the bus's default maximum message size, a +// 1695/AC7, AC10 (each node reads only the bucket of its own name): (untagged on purpose: the merge check of the trunk runs the tagless suite against its throwaway bus, and the +// test skips without MESH_TEST_NATS) against a real bus with the bucket of its own node, over the bus's default maximum message size, a // node-engine's link reads three objects that together are over 3 MiB by name, verifies each with File.Verify, and // refuses an object that is absent, one larger than the declaration says, and one that differs by a byte. // @@ -21,12 +21,29 @@ import ( // The bucket is made here: in the mesh the controller makes and fills it, and the node-engine only reads. func TestFilesAreFetchedByDigestFromTheBucket(t *testing.T) { conn, js := aBus(t) - store, err := js.ObjectStore(declaration.FileBucket) + const node = "filesnode" + bucket, err := declaration.FileBucketFor(node) if err != nil { - if store, err = js.CreateObjectStore(&nats.ObjectStoreConfig{Bucket: declaration.FileBucket}); err != nil { + t.Fatal(err) + } + store, err := js.ObjectStore(bucket) + if err != nil { + if store, err = js.CreateObjectStore(&nats.ObjectStoreConfig{Bucket: bucket}); err != nil { t.Fatal(err) } } + // Another node's bucket holds an object this node's engine must never be able to read, whatever its name. + otherBucket, _ := declaration.FileBucketFor("othernode") + other, err := js.ObjectStore(otherBucket) + if err != nil { + if other, err = js.CreateObjectStore(&nats.ObjectStoreConfig{Bucket: otherBucket}); err != nil { + t.Fatal(err) + } + } + foreign := strings.Repeat("2", 64) + if _, err := other.PutBytes(foreign, []byte("another node's file")); err != nil { + t.Fatal(err) + } var files []*declaration.File bodies := map[string][]byte{} total := 0 @@ -50,7 +67,7 @@ func TestFilesAreFetchedByDigestFromTheBucket(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - bus := OverNATS{Conn: conn, JS: js} + bus := OverNATS{Conn: conn, JS: js, Node: node} fetch := FilesFrom(WithFiles(ctx, bus)) if fetch == nil { t.Fatal("a link over the bus carries no fetch") @@ -73,6 +90,12 @@ func TestFilesAreFetchedByDigestFromTheBucket(t *testing.T) { if _, err := fetch(ctx, name, files[0].Size-1); err == nil || !strings.Contains(err.Error(), "larger") { t.Errorf("an object larger than the declaration says was read: %v", err) } + if _, err := fetch(ctx, foreign, 100); err == nil { + t.Error("1695/AC10: an object of another node's bucket was read") + } + if _, err := (OverNATS{Conn: conn, JS: js, Node: "bad.node"}).FileObject(ctx, name, 10); err == nil { + t.Error("1695/AC10: a node name that is not a bucket token was taken") + } absent := strings.Repeat("0", 64) if _, err := fetch(ctx, absent, 10); err == nil { t.Error("an absent object was read") diff --git a/internal/link/files_node_test.go b/internal/link/files_node_test.go new file mode 100644 index 0000000..18191a5 --- /dev/null +++ b/internal/link/files_node_test.go @@ -0,0 +1,17 @@ +package link + +import ( + "context" + "strings" + "testing" +) + +// 1695/AC10: a link with no node name, or a name that is not a bucket token, reads nothing: it cannot name its own bucket. +func TestAFileIsReadOnlyFromTheBucketOfTheNodesOwnName(t *testing.T) { + for _, node := range []string{"", "a.b", "a*", "a>", "a b"} { + _, err := OverNATS{Node: node}.FileObject(context.Background(), strings.Repeat("0", 64), 10) + if err == nil { + t.Errorf("1695/AC10: node %q read an object", node) + } + } +} -- 2.54.0 From f9e1cd183af158fd91192727e08343fcf1a972c9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 15:49:44 +0200 Subject: [PATCH 2/3] Pin mesh-sdk to the tip of fix/1694-bucket-per-node (issue 1695) --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index cb2d687..00195b8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 golang.org/x/sys v0.48.0 diff --git a/go.sum b/go.sum index 0cc0e88..ca8f649 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e h1:eq29ENik git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9 h1:FSepuLYomAwsIsPTUhSs31kFwCJ+XIXfNUIcGYPwcsI= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508 h1:nTQN4W6gfIUR0fLpA1IYb06TGr3MtuW0xtznjbcMGLM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -- 2.54.0 From e3a595d8fe1b8ee7c141020640ac59890e4f15d7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 18:03:34 +0200 Subject: [PATCH 3/3] Pin mesh-sdk to the merge of #64 (a bucket per node) --- go.mod | 2 +- go.sum | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 00195b8..df20074 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021153908-725590e06e75 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 golang.org/x/sys v0.48.0 diff --git a/go.sum b/go.sum index ca8f649..6ecfe04 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020085642-ab0c16ddad26 h1:oT1l3ZdjyF82MK14QMLKzoH2+iOFH/+UEYdlK3T2f7k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020085642-ab0c16ddad26/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f h1:7J/9jOfSfjfYpf8MOj96HwQRViQUkBNMb71RHP2mjn8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e h1:eq29ENikw3XfEXjxL5OdnTbavv5pu1/UnQhWWuf5V5I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9 h1:FSepuLYomAwsIsPTUhSs31kFwCJ+XIXfNUIcGYPwcsI= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021133938-36d9eba51db9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508 h1:nTQN4W6gfIUR0fLpA1IYb06TGr3MtuW0xtznjbcMGLM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021134822-86f9a1a5c508/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021153908-725590e06e75 h1:AcYfyJpYaUufhI93VG76+b001msXY4XX7kuRjnf33HY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021153908-725590e06e75/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -- 2.54.0