grantsFor granted every consumer a pair credential from the provider was ever made for, so a consumer pinned back to its own store was still asked of the store it left, which then never retired it. A credential whose consumer's resolution binds it elsewhere is now withdrawn like one nobody asks for, kept on record for the login the provider keeps, and said on plan and push.
151 lines
6.3 KiB
Go
151 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// novox/hq ADR 0225, issue 263: a consumer's identity is bounded by the provision it requires, an
|
|
// overflow is refused before merge by `module check`, and a provider's machine is never refused for
|
|
// one consumer's identity.
|
|
|
|
// `module check` refuses the pull request that introduces an overflow, naming the module.
|
|
func TestModuleCheckRefusesAnIdentityThatOverflowsWhatItRequires(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write := func(name, body string) string {
|
|
p := filepath.Join(dir, name+".json")
|
|
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
objects := write("objects", `{"module":"objects","version":"1",
|
|
"provides":[{"name":"s3-bucket","scope":"mesh","identity":{"max":20,"in":"an S3 access key"}}],
|
|
"receives":{"s3-bucket":"/var/lib/mesh/objects/mesh.json"}}`)
|
|
resolver := write("resolver", `{"module":"resolver","version":"1",
|
|
"provides":[{"name":"wildcard-resolution","scope":"mesh","identity":false}]}`)
|
|
album := write("photoalbum", `{"module":"photoalbum","version":"1","requires":["s3-bucket"]}`)
|
|
nm := write("networkmanager", `{"module":"networkmanager","version":"1","requires":["wildcard-resolution"]}`)
|
|
|
|
var out bytes.Buffer
|
|
if err := moduleCheckFor([]string{resolver, nm}, 6, &out); err != nil {
|
|
t.Fatalf("a long name requiring a keyless provision was refused (issue 263): %v\n%s", err, out.String())
|
|
}
|
|
out.Reset()
|
|
err := moduleCheckFor([]string{objects, album, resolver, nm}, 6, &out)
|
|
if err == nil {
|
|
t.Fatalf("an identity overflowing an S3 access key passed:\n%s", out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "photoalbum wants s3-bucket") ||
|
|
!strings.Contains(out.String(), "`slug` of at most 8 characters") ||
|
|
strings.Contains(out.String(), "networkmanager wants") {
|
|
t.Fatalf("the refusal does not name the one overflowing module and its remedy:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
// Tonight's case, through the commands: networkmanager on a six-character machine requires the
|
|
// resolver provision, and a second consumer there overflows an object store's access key. The
|
|
// provider's machine still composes; the overflowing consumer is left out of its grants and named,
|
|
// by push and by `status`, and the keyless consumer is granted with its long name.
|
|
func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "objects", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh,
|
|
Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}},
|
|
Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}})
|
|
register(t, open, catalogue.Manifest{Module: "resolver", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}}})
|
|
register(t, open, catalogue.Manifest{Module: "networkmanager", Version: "1",
|
|
Requires: []string{"wildcard-resolution"}})
|
|
register(t, open, catalogue.Manifest{Module: "photoalbum", Version: "1", Requires: []string{"s3-bucket"}})
|
|
register(t, open, catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"}})
|
|
for _, a := range [][2]string{{"anchor", "objects"}, {"anchor", "resolver"},
|
|
{"laptop", "networkmanager"}, {"laptop", "photoalbum"}, {"laptop", "files"}} {
|
|
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
|
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
|
|
}
|
|
}
|
|
|
|
// The consumer's machine resolves, and says which of its modules no provider will grant.
|
|
consumer, _, err := planFor(ctx, open, "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
over := consumer.Overflowing()
|
|
if len(over) != 1 || over[0].Module != "photoalbum" || over[0].Provision != "s3-bucket" {
|
|
t.Fatalf("the consumer's side does not name exactly photoalbum: %+v", over)
|
|
}
|
|
|
|
// The provider's machine composes. Under ADR 0049's one bound this was a refusal naming
|
|
// networkmanager, and no push to the provider could go through.
|
|
plan, settings, err := planFor(ctx, open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared, err := declarationFor(ctx, open, "anchor", plan, settings)
|
|
if err != nil {
|
|
t.Fatalf("one consumer's identity refused its provider's whole machine: %v", err)
|
|
}
|
|
if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" {
|
|
t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld)
|
|
}
|
|
grants, _, _, err := grantsFor(ctx, open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keyless bool
|
|
for _, g := range grants {
|
|
keyless = keyless || g.Provision == "wildcard-resolution" && g.From == "networkmanager"
|
|
}
|
|
if !keyless {
|
|
t.Fatalf("networkmanager, 26 characters, is not granted the keyless resolver provision: %+v", grants)
|
|
}
|
|
var granted []string
|
|
for _, c := range declared.Received["objects"]["s3-bucket"] {
|
|
granted = append(granted, c.From)
|
|
}
|
|
if strings.Join(granted, ",") != "files" {
|
|
t.Fatalf("the object store grants %v; files and only files fit", granted)
|
|
}
|
|
said := printed(t, func() error { reportLeftOut("anchor", declared); return nil })
|
|
if !strings.Contains(said, `photoalbum on laptop requires s3-bucket from anchor`) ||
|
|
!strings.Contains(said, "left out of anchor's grants") {
|
|
t.Fatalf("the push does not say whom it leaves out:\n%s", said)
|
|
}
|
|
|
|
// And `status` names it, and does not call the mesh well while it stands.
|
|
asked, err := theThreeQuestions(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked.overflowing) != 1 || asked.overflowing[0].Module != "photoalbum" {
|
|
t.Fatalf("status does not carry the overflow: %+v", asked.overflowing)
|
|
}
|
|
if asked.well() {
|
|
t.Fatal("a mesh with a consumer left out of its grants reads as well")
|
|
}
|
|
shown := printed(t, func() error { return printStatus(asked) })
|
|
if !strings.Contains(shown, "identified too long for a provision they require") ||
|
|
!strings.Contains(shown, "mesh_laptop_photoalbum") {
|
|
t.Fatalf("status does not say it:\n%s", shown)
|
|
}
|
|
body, err := statusAsJSON(asked)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc struct {
|
|
Overflowing []catalogue.Overflow `json:"overflowing"`
|
|
}
|
|
if err := json.Unmarshal(body, &doc); err != nil || len(doc.Overflowing) != 1 ||
|
|
doc.Overflowing[0].Bound.Max != 20 {
|
|
t.Fatalf("the document does not carry it: %v\n%s", err, body)
|
|
}
|
|
}
|