Read a declared file from the node's own object bucket, declaration-files-<node> (issue 1695, ADR 0488) #128
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
+15
-8
@@ -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-<node>), 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-<this node>, 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 (
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user