diff --git a/go/delivery/own_drift.go b/go/delivery/own_drift.go new file mode 100644 index 0000000..fac57fa --- /dev/null +++ b/go/delivery/own_drift.go @@ -0,0 +1,84 @@ +package delivery + +// The own secrets a node's shadow answer finds the vault holds another value for than the node runs (novox/hq issue 1126, +// cause C4): the vault's `move-in` never replaces a value it holds, so a value the controller made anew later (a rotation +// at start, a re-issue) runs while the vault keeps the first one it was handed. The node opens both seals with its own +// key and its answer's shown bodies mask the value as MaskedAnotherValue; this reads which own secrets those are, for the +// node agent (which hands the vault what runs) and for the vault's `own_drift` (which lists them mesh-wide), so both read +// the answer one way. + +import ( + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// MaskedAnotherValue is the word a shown body masks a value with when the node opened both sides with its key and they +// are two values (MaskedPair, maskWord). +const MaskedAnotherValue = "" + +// OwnValueDiffers is one own secret of a module whose value composed (the vault's) is another than the one that runs. +type OwnValueDiffers struct { + Module string `json:"module"` + Name string `json:"name"` +} + +// ownNeedPrefix starts the id of the file a module's own secret is placed in (catalogue.NeedID). +var ownNeedPrefix = catalogue.NeedID("") + +// OwnValuesDiffer is every own secret a's shown bodies say holds another value composed than running: a body of a +// module's own secret's file (catalogue.NeedID) masked MaskedAnotherValue on either side. Sorted by module and name, each +// once. A body the answer did not show (beyond MaxBodies) is not read. +func OwnValuesDiffer(a CandidateAnswer) []OwnValueDiffers { + seen := map[OwnValueDiffers]bool{} + var out []OwnValueDiffers + for _, d := range a.Differs { + for _, b := range d.Bodies { + name, own := strings.CutPrefix(b.ID, ownNeedPrefix) + if !own || name == "" || !anotherValue(b) { + continue + } + o := OwnValueDiffers{Module: d.Module, Name: name} + if !seen[o] { + seen[o] = true + out = append(out, o) + } + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].Module != out[j].Module { + return out[i].Module < out[j].Module + } + return out[i].Name < out[j].Name + }) + return out +} + +// ValuesDiffer is how many shown bodies of a hold a value masked MaskedAnotherValue, own secrets and every other +// resource alike: the count a node's answer carries as CandidateAnswer.ValuesDiffer. +func ValuesDiffer(a CandidateAnswer) int { + n := 0 + for _, d := range a.Differs { + for _, b := range d.Bodies { + if anotherValue(b) { + n++ + } + } + } + return n +} + +// WithValuesDiffer is answers with each one's ValuesDiffer counted. +func WithValuesDiffer(answers map[string]CandidateAnswer) map[string]CandidateAnswer { + out := make(map[string]CandidateAnswer, len(answers)) + for g, a := range answers { + a.ValuesDiffer = ValuesDiffer(a) + out[g] = a + } + return out +} + +func anotherValue(b ResourceBodies) bool { + return strings.Contains(b.Composed, MaskedAnotherValue) || strings.Contains(b.Running, MaskedAnotherValue) +} diff --git a/go/delivery/placement_candidate.go b/go/delivery/placement_candidate.go index 12f276e..d6ae907 100644 --- a/go/delivery/placement_candidate.go +++ b/go/delivery/placement_candidate.go @@ -141,6 +141,10 @@ type CandidateAnswer struct { // Note is what the node says of how it answered, in words, beside the verdict: a pass that ran over a minute, inputs // it waited for (issue 1126: the shadow shows its work). It changes no verdict. Note string `json:"note,omitempty"` + // ValuesDiffer is how many shown bodies hold a value the node opened on both sides to two values (ValuesDiffer, + // MaskedAnotherValue): the vault's value and the one that runs drifted apart (issue 1126, cause C4). It changes no + // verdict. + ValuesDiffer int `json:"values_differ,omitempty"` } // ReturnsToController is what a node answers a head that deletes its placement file. diff --git a/go/delivery/placement_test.go b/go/delivery/placement_test.go index cde72a4..8f43377 100644 --- a/go/delivery/placement_test.go +++ b/go/delivery/placement_test.go @@ -354,6 +354,23 @@ func TestPlacement(t *testing.T) { !strings.Contains(c, "") { t.Errorf("masked as %s and %s", c, r) } + // Issue 1126, cause C4: the own secrets whose two sides open to two values are read from the shown bodies, and + // every body so masked is counted, own or not. + drift := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("needs-jwt", "box-1", "0600"), + sealedFile("needs-salt", "box-4", "0600"), sealedFile("conf", "box-6", "0600"))}, + []Composed{comp(sealedFile("needs-jwt", "box-2", "0600"), sealedFile("needs-salt", "box-5", "0600"), + sealedFile("conf", "box-7", "0600"))}, + func(a, b string) (same, known bool) { return a == "box-4", true }) + got := CandidateAnswer{Head: "h", Differs: drift.Differs} + if own := OwnValuesDiffer(got); len(own) != 1 || own[0] != (OwnValueDiffers{Module: "nats", Name: "jwt"}) { + t.Errorf("the own secrets with another value are %+v, want nats jwt alone", own) + } + if n := ValuesDiffer(got); n != 2 { + t.Errorf("%d bodies hold another value, want 2 (jwt and conf)", n) + } + if w := WithValuesDiffer(map[string]CandidateAnswer{"g": got}); w["g"].ValuesDiffer != 2 { + t.Errorf("the answer carries values_differ %d", w["g"].ValuesDiffer) + } if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" { t.Errorf("the node's own composition run and not composed is removed: %+v", c) }