package compose import ( "encoding/json" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/overlay" ) // secretValue is a value the Input carries that must never appear in anything the composition says. const secretValue = "SECRET-VALUE-THAT-MUST-NOT-LEAK" func rendered(t *testing.T, in Input, node string, mode Mode) Rendered { t.Helper() got, err := Render(in, node, mode) if err != nil { t.Fatalf("%v", err) } return got } // anAssistant is mesh-controller's outside_not_given_test.go anAssistant (cmd/mesh-controller at 6c2a8d62): a module with a secret // the mesh makes and one only an outside party issues. func anAssistant() catalogue.Manifest { return catalogue.Manifest{Module: "assistant", Version: "1", OwnSecrets: catalogue.OwnSecrets{ "password": {Path: "/var/lib/mesh/assistant/password", Taken: catalogue.TakenAtStart}, "vendor-key": {Path: "/var/lib/mesh/assistant/vendor-key", Taken: catalogue.TakenAtStart, IssuedBy: catalogue.IssuedOutside}, }, Resources: []map[string]any{{"id": "state", "type": "directory", "path": "/var/lib/mesh/assistant", "mode": "0700"}}} } func withAssistant(t *testing.T) Input { t.Helper() in := aMesh(t) in.Catalogue["assistant"] = anAssistant() laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "assistant") laptop.SealingKey = keyOf(t, "laptop") return in } // Own secrets (plan.go renderingFor, novox/hq issue 275 and 401): one held and sealed to the machine's key is carried; one not held is // foreseen in the Foreseeing mode and left out in the Reading mode, and nothing is made. func TestAnOwnSecretHeldIsCarriedAndOneNotHeldIsForeseenOrLeftOut(t *testing.T) { mk := func() Input { in := withAssistant(t) in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: nodeOf(t, &in, "laptop").SealingKey, Sealed: secretValue}} return in } in := mk() got := rendered(t, in, "laptop", Foreseeing) if got.Rendering.Needed["assistant"]["password"] != secretValue || len(got.Rendering.Foreseen) != 0 { t.Errorf("a held secret: needed %v foreseen %v", got.Rendering.Needed, got.Rendering.Foreseen) } // Not held: foreseen with a stand-in (Foreseeing), left out (Reading). in.OwnSecrets = nil got = rendered(t, in, "laptop", Foreseeing) if !got.Rendering.Foreseen["assistant"]["password"] || got.Rendering.Needed["assistant"]["password"] != "" || len(got.Foreseen) != 1 || got.Foreseen[0] != "assistant/password" { t.Errorf("a secret the send makes: foreseen %v / %v needed %v", got.Rendering.Foreseen, got.Foreseen, got.Rendering.Needed) } got = rendered(t, in, "laptop", Reading) if len(got.Rendering.Needed["assistant"]) != 0 || len(got.Foreseen) != 0 || got.Rendering.Foreseen != nil { t.Errorf("Reading composed a secret that was never issued: %v %v", got.Rendering.Needed, got.Foreseen) } // Held but sealed to a key the machine no longer has is not held. in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: "an-old-key", Sealed: secretValue}} if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["assistant"]["password"] { t.Errorf("a secret sealed to an old key is held: %v", got.Rendering.Needed) } // Held with no value in the Input: a stand-in, named. in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: nodeOf(t, &in, "laptop").SealingKey}} if got := rendered(t, in, "laptop", Reading); !got.Rendering.Foreseen["assistant"]["password"] { t.Errorf("a held secret with no value is not named: %v", got.Rendering.Foreseen) } unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) }) } // An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228): nothing is composed or // foreseen for it; a value the mesh made for it is not one a person gave; a value given is placed once it is sealed to the key held now. func TestAnOutsideSecretIsPlacedOnlyOnceAPersonGaveIt(t *testing.T) { in := withAssistant(t) key := nodeOf(t, &in, "laptop").SealingKey for _, mode := range []Mode{Reading, Foreseeing} { got := rendered(t, in, "laptop", mode) if _, there := got.Rendering.Needed["assistant"]["vendor-key"]; there || got.Rendering.Foreseen["assistant"]["vendor-key"] { t.Fatalf("%v: a secret nobody gave is composed: %v %v", mode, got.Rendering.Needed, got.Rendering.Foreseen) } } in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "made", SealedTo: key, Sealed: secretValue}} if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there { t.Error("a value the mesh made for a secret issued outside was placed") } in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "accepted", SealedTo: key, Sealed: "the vendor's key, sealed"}} if got := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; got != "the vendor's key, sealed" { t.Errorf("a value given is placed as %q", got) } in.OwnSecrets[0].SealedTo = "an-old-key" if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there { t.Error("a value given under an old key was placed") } } // 1364/AC3 (the send's refusals, novox/hq issue 203 and 275): in the Foreseeing mode a bus credential nobody issued and a given secret // under an old key are refused in the controller's words; with the credential issued, the secret is foreseen. No secret value is in // an error. func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) { broker := catalogue.Manifest{Module: "gate", Version: "1", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/gate/broker", Taken: catalogue.TakenAtStart}}} in := withAssistant(t) in.Catalogue["gate"] = broker laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "gate") _, err := Render(in, "laptop", Foreseeing) want := "gate on laptop has no bus credential: nothing was issued for laptop.gate, and a push would seal a placeholder " + "its process cannot read (novox/hq issue 203). `module issue gate --node laptop`, then push again" if err == nil || err.Error() != want { t.Fatalf("got %v, want %q", err, want) } // Reading refuses nothing for it: the machine is not running it. if _, err := Render(in, "laptop", Reading); err != nil { t.Fatalf("Reading refused a secret the machine is not running: %v", err) } in.BusMinted = []string{"laptop.gate"} if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["gate"]["broker"] { t.Errorf("an issued bus credential is not foreseen: %v", got.Rendering.Foreseen) } // A given secret under an old key. in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "accepted", SealedTo: "an-old-key", Sealed: secretValue}, {Node: "laptop", Module: "gate", Name: "broker", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}} _, err = Render(in, "laptop", Foreseeing) wantOld := "assistant on laptop holds \"password\", which was given to the mesh rather than made by it, and laptop has since generated a new " + "sealing key. The mesh cannot make another; issue it again" if err == nil || err.Error() != wantOld { t.Fatalf("got %v, want %q", err, wantOld) } if strings.Contains(err.Error(), secretValue) { t.Error("a secret value is in an error") } // A machine with no sealing key cannot be sealed to. laptop.SealingKey = "" in.OwnSecrets = nil if _, err := Render(in, "laptop", Foreseeing); err == nil || !strings.Contains(err.Error(), "laptop has no sealing key, so nothing can be sealed to it") { t.Fatalf("no sealing key: %v", err) } } // aGrantMesh is the objects provider on anchor and its consumer files on laptop, with the pair credential made. func aGrantMesh(t *testing.T) Input { t.Helper() in := theObjectsMesh(t, objects()) in.Issued = []Issued{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", ForProvider: "sealed-to-anchor"}} return in } // A credential made for a consumer is a grant on its provider's machine, carrying the consumer's values and the sealed copy; without // the sealed copy a stand-in is composed and named (novox/hq plan.go grantsFor). func TestAGrantIsCreatedForEachCredentialMadeForAConsumer(t *testing.T) { in := aGrantMesh(t) got := rendered(t, in, "anchor", Foreseeing) if len(got.Rendering.Grants) != 1 { t.Fatalf("grants are %+v", got.Rendering.Grants) } g := got.Rendering.Grants[0] if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.At != "laptop.internal" || g.Sealed != "sealed-to-anchor" { t.Errorf("the grant is %+v", g) } if len(got.Foreseen) != 0 || len(got.Withheld) != 0 || len(got.Unbound) != 0 { t.Errorf("foreseen %v withheld %v unbound %v", got.Foreseen, got.Withheld, got.Unbound) } in.Issued[0].ForProvider = "" got = rendered(t, in, "anchor", Foreseeing) if got.Rendering.Grants[0].Sealed != catalogue.ForeseenSealed || len(got.Foreseen) != 1 { t.Errorf("a grant with no sealed copy: %+v foreseen %v", got.Rendering.Grants, got.Foreseen) } // A credential made for a consumer on another provider is not this node's grant. if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 { t.Error("the consumer is granted its own credential") } // A consumer whose set does not resolve is skipped, not fatal. nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files") in.Catalogue["files"] = catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"}, Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}} in.Catalogue["twin"] = catalogue.Manifest{Module: "twin", Version: "1", Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}} nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "twin") if got, err := Render(in, "anchor", Foreseeing); err != nil || len(got.Rendering.Grants) != 0 { t.Errorf("a consumer that does not resolve: %v %+v", err, got.Rendering.Grants) } } // 1364/AC2: a consumer the grants leave out because its identity overflows the provision's bound is returned beside them, naming the // consumer and the provision (novox/hq ADR 0225), and is not granted. func TestAConsumerWhoseIdentityOverflowsIsLeftOutOfTheGrantsAndNamed(t *testing.T) { in := aGrantMesh(t) in.Nodes[1].Name = "a-machine-with-a-very-long-name" in.Issued[0].Consumer = "a-machine-with-a-very-long-name" got := rendered(t, in, "anchor", Foreseeing) if len(got.Rendering.Grants) != 0 || len(got.Withheld) != 1 { t.Fatalf("grants %+v withheld %+v", got.Rendering.Grants, got.Withheld) } w := got.Withheld[0] if w.Consumer != "a-machine-with-a-very-long-name" || w.Provision != "s3-bucket" || w.Provider != "anchor" || w.Module != "files" || !strings.Contains(w.String(), "a-machine-with-a-very-long-name") || !strings.Contains(w.String(), "s3-bucket") { t.Errorf("the consumer left out is %+v / %s", w, w.String()) } if len(got.Rendering.Withheld) != 1 { t.Errorf("the Rendering does not carry it: %+v", got.Rendering.Withheld) } } // 1364/AC3: a credential on record for a consumer that its own resolution binds to another provider is withdrawn, named with where it is // bound (novox/hq issue 274): the grant stays, with no consumer module asking. func TestACredentialBoundToAnotherProviderIsWithdrawnAndSaidWhereItIsBound(t *testing.T) { in := aGrantMesh(t) second := objects() second.Module = "objects2" in.Catalogue["objects2"] = second in.Nodes = append(in.Nodes, Node{Name: "ace", Place: Place{Site: "there", Address: "10.77.0.3"}, Assigned: []string{overlay.Name, "objects2"}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}}) nodeOf(t, &in, "laptop").Pins = map[string]catalogue.Chosen{"s3-bucket": {Node: "ace", Module: "objects2"}} got := rendered(t, in, "anchor", Foreseeing) if len(got.Unbound) != 1 { t.Fatalf("unbound %+v grants %+v", got.Unbound, got.Rendering.Grants) } u := got.Unbound[0] if u.Provision != "s3-bucket" || u.Provider != "anchor" || u.Consumer != "laptop" || u.Module != "files" || len(u.BoundTo) != 1 || u.BoundTo[0] != "ace" || !strings.Contains(u.String(), "ace") { t.Errorf("unbound is %+v / %s", u, u.String()) } if len(got.Rendering.Grants) != 1 || got.Rendering.Grants[0].From != "" { t.Errorf("the withdrawn credential is still asked for: %+v", got.Rendering.Grants) } } func aCertificateMesh(t *testing.T) Input { t.Helper() in := aMesh(t) in.Catalogue["secure"] = catalogue.Manifest{Module: "secure", Version: "1", Certificate: &catalogue.Certificate{Into: "/etc/secure/tls.pem", Authority: "/etc/secure/ca.pem"}} laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "secure") return in } // 1364/AC4: with no certificate and authority in the Input a stand-in is composed and named; given them, the composition carries them as // given. A machine that never reported a serving key, and one not on the private network, are refused in the controller's words. func TestACertificateIsCarriedAsGivenOrStoodInForAndNamed(t *testing.T) { in := aCertificateMesh(t) laptop := nodeOf(t, &in, "laptop") if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and has never told the mesh what key it serves with; it joins again to report one" { t.Fatalf("no serving key: %v", err) } laptop.ServingKey = "serving-key" got := rendered(t, in, "laptop", Foreseeing) if got.Rendering.Certificate != ForeseenCertificate || got.Rendering.Authority != ForeseenCertificate || len(got.Foreseen) != 1 { t.Errorf("no certificate in the Input: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen) } laptop.Certificate, in.Authority = "the-certificate", "the-authority" got = rendered(t, in, "laptop", Foreseeing) if got.Rendering.Certificate != "the-certificate" || got.Rendering.Authority != "the-authority" || len(got.Foreseen) != 0 { t.Errorf("a certificate given: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen) } laptop.Place.Address = "" // placed nowhere: no name inside the mesh if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and is not on the private network, so it has no name inside the mesh to be certified for" { t.Fatalf("off the network: %v", err) } } func aValidKey(t *testing.T, name string) string { return keyOf(t, name) } // 1364/AC5 and AC6: a core credential the node needs and the Input holds no box for is composed with a stand-in and returned as one // to keep; a kept box sealed to the key held now is carried; a node-engine below the vault-box contract is sent none (and nothing is // asked of it); a machine with no sealing key has its core module left out, said. func TestACoreBoxTheInputHoldsNoneOfIsReturnedToKeepAndNothingIsWritten(t *testing.T) { mk := func() Input { in := aMesh(t) in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}} laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "nats") laptop.SealingKey = aValidKey(t, "laptop") laptop.Stated, laptop.Contract = true, VaultBoxContract in.BusMinted = []string{"laptop.nats"} return in } in := mk() got := rendered(t, in, "laptop", Foreseeing) box := got.Rendering.VaultSealed["nats"]["broker"] if string(box) != string(catalogue.ForeseenVaultBox) || len(got.CoreBoxesWanted) != 1 || got.CoreBoxesWanted[0] != (CoreBoxWanted{Node: "laptop", Module: "nats", Name: "broker"}) { t.Errorf("no box held: %s wanted %v", box, got.CoreBoxesWanted) } kept := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`) in.KeptCoreBoxes = []CoreBox{{Node: "laptop", Module: "nats", Name: "broker", Generation: 3, SealedTo: nodeOf(t, &in, "laptop").SealingKey, Box: kept}} got = rendered(t, in, "laptop", Foreseeing) if string(got.Rendering.VaultSealed["nats"]["broker"]) != string(kept) || len(got.CoreBoxesWanted) != 0 { t.Errorf("a kept box: %s wanted %v", got.Rendering.VaultSealed["nats"]["broker"], got.CoreBoxesWanted) } in.KeptCoreBoxes[0].SealedTo = "an-old-key" if got = rendered(t, in, "laptop", Foreseeing); len(got.CoreBoxesWanted) != 1 { t.Errorf("a box sealed to an old key is kept: %v", got.CoreBoxesWanted) } // Below the vault-box contract: none, and the controller's own seal as before. nodeOf(t, &in, "laptop").Contract = VaultBoxContract - 1 if got = rendered(t, in, "laptop", Foreseeing); got.Rendering.VaultSealed != nil || len(got.CoreBoxesWanted) != 0 { t.Errorf("an engine below the contract is sent boxes: %v %v", got.Rendering.VaultSealed, got.CoreBoxesWanted) } // No sealing key: the module is left out, said. nodeOf(t, &in, "laptop").Contract, nodeOf(t, &in, "laptop").SealingKey = VaultBoxContract, "" got = rendered(t, in, "laptop", Reading) if why := got.Rendering.NotPlaced["nats"]; !strings.Contains(why, "laptop has no sealing key, so the vault can seal nats's credentials to nothing there") { t.Errorf("not placed: %v", got.Rendering.NotPlaced) } unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) }) } // 1364/AC1: the bus's user list is carried for the machine that runs the bus and for no other; without it a stand-in is composed and // named; the users left out for want of a credential are said; each machine's membership is carried. (The list is composed by the // controller from the bus records, which stay the controller's: see the commit message.) func TestTheBusUserListIsCarriedForTheMachineThatRunsTheBus(t *testing.T) { in := aMesh(t) in.Catalogue["bus"] = catalogue.Manifest{Module: "bus", Version: "1", BusUsers: "/etc/bus/accounts.conf", Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "bus") in.BusUsers = "accounts { anchor.bus: hash }" in.BusMissing = []string{"laptop.files", "laptop.web"} in.BusMemberships = map[string]string{"laptop": "sealed-membership"} got := rendered(t, in, "anchor", Foreseeing) if got.Rendering.BusUsers != in.BusUsers || len(got.Foreseen) != 0 { t.Errorf("the list is %q foreseen %v", got.Rendering.BusUsers, got.Foreseen) } if len(got.Said) != 1 || got.Said[0] != "the bus's user list leaves out 2 user(s) the mesh has minted no credential for: laptop.files, laptop.web. Each is a user that cannot connect until one is issued" { t.Errorf("said %v", got.Said) } if other := rendered(t, in, "laptop", Foreseeing); other.Rendering.BusUsers != "" || other.Rendering.BusMembership != "sealed-membership" { t.Errorf("laptop: list %q membership %q", other.Rendering.BusUsers, other.Rendering.BusMembership) } in.BusUsers = "" got = rendered(t, in, "anchor", Foreseeing) if got.Rendering.BusUsers != ForeseenBusUsers || len(got.Foreseen) != 1 { t.Errorf("no list in the Input: %q foreseen %v", got.Rendering.BusUsers, got.Foreseen) } } // 1364/AC7: nothing the composition says carries a secret value the Input holds. func TestNoSecretValueOfTheInputIsInWhatTheCompositionSays(t *testing.T) { in := aGrantMesh(t) in.Issued[0].ForProvider = secretValue laptop := nodeOf(t, &in, "laptop") in.Catalogue["assistant"] = anAssistant() laptop.Assigned = append(laptop.Assigned, "assistant") laptop.SealingKey = keyOf(t, "laptop") in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}} in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld, Sealed: secretValue}} for _, node := range []string{"anchor", "laptop"} { got := rendered(t, in, node, Foreseeing) for _, said := range append(append([]string{}, got.Foreseen...), got.Said...) { if strings.Contains(said, secretValue) { t.Errorf("%s: %q carries a secret value", node, said) } } } }