diff --git a/go.mod b/go.mod index 8cde6e6..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.20261021130958-85b5a2687e6a + 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 cba2076..6ecfe04 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +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.20261021130958-85b5a2687e6a h1:CVTQBD4vxbGQ+TW9MWAut/ABQN6nvAZ1VTqojNpvGUE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021130958-85b5a2687e6a/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= 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) + } + } +}