diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 6686e17..a1f4505 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -573,6 +573,28 @@ func ParseManifest(raw []byte) (Manifest, error) { m.Module, c.Authority)) } } + // **A module may not declare an action, and this is where it is said** (novox/hq ADR 0005). + // + // The host already refuses one, correctly and for the right reason: the link may not carry a + // command to run, and that bound is what limits a compromised control plane to shapes it + // cannot turn into arbitrary code. But a module's resources reach a machine over the link, so + // a manifest carrying an action was accepted here, stored, resolved, planned and pushed — and + // refused on the machine, in the host's log, with nothing connecting it back to the manifest + // that caused it. + // + // That is the same failure as the network shape earlier today: the refusal was right, arrived + // far from its cause, and nobody was reading the log. A rule enforced only at the far end is + // enforced; it is just not usable. + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) != "action" { + continue + } + problems = append(problems, fmt.Sprintf( + "%s declares %v, which is an action, and a module may not: the link may not carry a "+ + "command to run (novox/hq ADR 0005). A module that needs something done ships a "+ + "program that reads what the mesh delivered and reconciles", + m.Module, r["id"])) + } for name, where := range m.OwnSecrets { if !strings.HasPrefix(where, "/") { problems = append(problems, fmt.Sprintf( diff --git a/internal/catalogue/naming_test.go b/internal/catalogue/naming_test.go index 617730f..c168ea4 100644 --- a/internal/catalogue/naming_test.go +++ b/internal/catalogue/naming_test.go @@ -37,3 +37,39 @@ func TestARoleNameIsFineWhereTheConsumerCannotTellTheDifference(t *testing.T) { } } } + +// A module may not declare an action, and it is refused where it was written. +// +// The host refuses one arriving over the link, which is the bound the whole security argument +// rests on (novox/hq ADR 0005) and is enforced in the right place. But a module's resources reach +// a machine *over* the link, so a manifest carrying an action used to be accepted here, stored, +// resolved and pushed — and then refused on the machine, in a log, with nothing tying it back to +// the manifest. Enforced at the far end only is enforced and not usable. +func TestAModuleMayNotDeclareAnAction(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"probe","version":"1","resources":[ + {"id":"migrate","type":"action","command":["/bin/true"],"verify":["/bin/true"]}]}`)) + if err == nil { + t.Fatal("a manifest declaring an action was accepted, and the machine would refuse it") + } + if !strings.Contains(err.Error(), "may not") || !strings.Contains(err.Error(), "0005") { + t.Errorf("the refusal does not say what rule it is: %v", err) + } + // And it says what to do instead, because "you may not" without an alternative is where a + // module author stops. + if !strings.Contains(err.Error(), "ships a program") { + t.Errorf("the refusal names no alternative: %v", err) + } +} + +// Every other shape a module may declare still passes, so the check above bounds one thing. +func TestTheOtherShapesAreStillAccepted(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"probe","version":"1","resources":[ + {"id":"d","type":"directory","path":"/var/lib/probe","mode":"0700"}, + {"id":"f","type":"file","path":"/var/lib/probe/x","content":"x\n"}, + {"id":"n","type":"network","name":"probe"}, + {"id":"c","type":"container","name":"probe","image":"probe@sha256:` + + `0000000000000000000000000000000000000000000000000000000000000000"}]}`)) + if err != nil { + t.Fatalf("an ordinary manifest was refused: %v", err) + } +}