diff --git a/go/compose/composed.go b/go/compose/composed.go index 9fe66a3..651357d 100644 --- a/go/compose/composed.go +++ b/go/compose/composed.go @@ -2,8 +2,10 @@ package compose import ( "encoding/json" + "errors" "fmt" "sort" + "strings" "git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -16,6 +18,26 @@ import ( // (mesh-hand-compose) gets that one. Each module's Data says whether its manifest declares data. The compositions are not // signed and not yet by digest: the writer does both (delivery.SignComposed, Composed.ByDigest). func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) { + return compositions(in, node, fps, mode, false) +} + +// ErrStoodIn is a composition that holds a stand-in for a value the input does not carry (an own secret, a grant's +// sealed copy, a vault box, the certificate, the bus's user list) or a port it does not give: what a send would make, +// never what may be applied. +var ErrStoodIn = errors.New("compose: the composition stands in for what its input does not carry") + +// BusUsersStandIn is what a composition says it stood in for when its input carries no bus user list. +const BusUsersStandIn = "the bus's user list" + +// CompositionsApplied is Compositions for a node that applies what it composed itself (novox/hq ADR 0547 §2, issue +// 1126): a declaration that stands in for anything (Declared.StoodIn, Foreseen, ForeseenPorts) is refused with +// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list +// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself. +func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]delivery.Composed, []string, error) { + return compositions(in, node, fps, Reading, true) +} + +func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped []string, err error) { reading := in reading.Nodes = append([]Node(nil), in.Nodes...) found := false @@ -35,6 +57,24 @@ func Compositions(in Input, node string, fps map[string]string, mode Mode) (out if len(problems) > 0 { return nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems) } + if applied { + var stood []string + for _, x := range d.StoodIn { + // The bus's user list is the bus node's own to follow, written alone and never from a composition the node + // made itself (ADR 0547 §7, §8): its stand-in is held out of the apply, not applied. + if x != BusUsersStandIn { + stood = append(stood, x) + } + } + stood = append(stood, d.Foreseen...) + for _, p := range d.ForeseenPorts { + stood = append(stood, "port "+p) + } + if len(stood) > 0 { + sort.Strings(stood) + return nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", ")) + } + } body, err := d.Body() if err != nil { return nil, nil, err diff --git a/go/compose/credentials_test.go b/go/compose/credentials_test.go index ba4db0c..924df06 100644 --- a/go/compose/credentials_test.go +++ b/go/compose/credentials_test.go @@ -2,6 +2,7 @@ package compose import ( "encoding/json" + "errors" "strings" "testing" @@ -78,6 +79,16 @@ func TestAnOwnSecretHeldIsCarriedAndOneNotHeldIsForeseenOrLeftOut(t *testing.T) 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) }) + + // 1126 (ADR 0547 §2): a node applying what it composed itself never applies a stand-in: a held secret with no value in the + // Input is refused, naming it; held with its value, it composes. + fps := map[string]string{"assistant": "src2:" + strings.Repeat("a", 64), overlay.Name: "src2:" + strings.Repeat("b", 64)} + if _, _, err := CompositionsApplied(in, "laptop", fps); !errors.Is(err, ErrStoodIn) || !strings.Contains(err.Error(), "assistant/password") { + t.Errorf("a stand-in was not refused for a node's own apply: %v", err) + } + if out, _, err := CompositionsApplied(mk(), "laptop", fps); err != nil || len(out) == 0 { + t.Errorf("a held secret with its value is refused for a node's own apply: %v", err) + } } // An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228): nothing is composed or diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index 033bbcb..0c7386a 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -201,14 +201,15 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er return in, nil } -// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.Compositions in the Reading -// mode at the fingerprints fps gives each module, as the controller composes them. +// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the +// fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it +// stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed. func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) { in, err := Input(ctx, r, mesh, s) if err != nil { return nil, nil, err } - return compose.Compositions(in, s.Node, fps, compose.Reading) + return compose.CompositionsApplied(in, s.Node, fps) } // nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person diff --git a/go/compose/render.go b/go/compose/render.go index a50a65e..e8dca30 100644 --- a/go/compose/render.go +++ b/go/compose/render.go @@ -113,7 +113,7 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) { r.BusUsers = c.in.BusUsers if r.BusUsers == "" { r.BusUsers = ForeseenBusUsers - foreseen("the bus's user list") + foreseen(BusUsersStandIn) } if len(c.in.BusMissing) > 0 { out.Said = append(out.Said, fmt.Sprintf("the bus's user list leaves out %d user(s) the mesh has minted no credential "+