Seal a core credential by the controller where the vault is silent, and refuse the bus machine where no user has a credential (issues 1385, 1383)
Where the vault did not answer for a core credential and no box is kept, ADR 0396 has the controller seal it itself until the vault holds it: perform names those modules, and the send's declaration carries the controller's own seal in place of the stand-in vault box, as the controller's composition sent it. The machine that runs the bus is refused with busUserList's words where no user has a credential, as before; no other machine is. A machine whose recorded module cannot be kept is refused alone.
This commit is contained in:
@@ -33,8 +33,15 @@ import (
|
||||
//
|
||||
// perform returns whether it made or recorded anything: the reason the caller reads the stores again.
|
||||
func perform(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, error) {
|
||||
did, _, err := performSend(ctx, open, in, node, d)
|
||||
return did, err
|
||||
}
|
||||
|
||||
// performSend is perform, and the core modules whose credentials the vault did not answer for with no box kept: the
|
||||
// controller seals those itself until the vault holds them (core_box.go, ADR 0396), and the send carries that seal.
|
||||
func performSend(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, []string, error) {
|
||||
did, sealedByController := false, []string(nil)
|
||||
inv := open.inventory
|
||||
did := false
|
||||
|
||||
// The seats passed on: recorded in the store with the controller's own hand act (ADR 0449 §4, issue 1317).
|
||||
if len(d.Passes.Passes) > 0 {
|
||||
@@ -72,7 +79,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
// down. The error is the one planFor met: the machine that needs it, and why none could be made.
|
||||
for _, p := range in.wouldMake {
|
||||
if err := makePair(ctx, inv, p); err != nil {
|
||||
return did, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w",
|
||||
return did, nil, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w",
|
||||
p.ConsumerModule, p.Consumer, p.Name, p.Provider, err)
|
||||
}
|
||||
did = true
|
||||
@@ -82,13 +89,13 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
if len(d.ForeseenPorts) > 0 {
|
||||
shelf, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return did, err
|
||||
return did, nil, err
|
||||
}
|
||||
for _, one := range d.ForeseenPorts {
|
||||
module, port, ok := strings.Cut(one, ":")
|
||||
wanted, perr := strconv.Atoi(port)
|
||||
if !ok || perr != nil {
|
||||
return did, fmt.Errorf("the composition named a port the controller cannot read: %q", one)
|
||||
return did, nil, fmt.Errorf("the composition named a port the controller cannot read: %q", one)
|
||||
}
|
||||
fixed := false
|
||||
for _, l := range nodeShelf(in, shelf, node)[module].Listens {
|
||||
@@ -97,7 +104,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
}
|
||||
}
|
||||
if _, err := inv.PortFor(ctx, node, module, wanted, fixed); err != nil {
|
||||
return did, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err)
|
||||
return did, nil, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err)
|
||||
}
|
||||
did = true
|
||||
}
|
||||
@@ -111,7 +118,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
continue // a grant's sealed copy, the certificate or the bus's list: made below or by nobody
|
||||
}
|
||||
if _, err := inv.SecretForModule(ctx, node, module, name); err != nil {
|
||||
return did, err
|
||||
return did, nil, err
|
||||
}
|
||||
did = true
|
||||
}
|
||||
@@ -121,7 +128,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
if ok && n.Stated && n.Contract >= compose.VaultBoxContract {
|
||||
resolved, err := compose.Resolve(in.Input, node)
|
||||
if err != nil {
|
||||
return did, err
|
||||
return did, nil, err
|
||||
}
|
||||
hands := n.Stated && n.Contract >= compose.HandedContract
|
||||
handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)}
|
||||
@@ -129,30 +136,27 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
return handing.Handing(m)
|
||||
}, Allocating)
|
||||
if err != nil {
|
||||
return did, err
|
||||
return did, nil, err
|
||||
}
|
||||
if len(boxes.keep) > 0 {
|
||||
did = true
|
||||
}
|
||||
// A core credential the vault did not answer for and no box was kept for is composed by the controller's own seal
|
||||
// (core_box.go, ADR 0396); the composition over an Input has no such fallback, so the send is refused, said.
|
||||
if len(boxes.byController) > 0 {
|
||||
return did, fmt.Errorf("the vault did not answer for the core credentials of %s (%s) and no box of theirs is kept: "+
|
||||
"nothing is sent until it does", node, strings.Join(boxes.byController, ", "))
|
||||
}
|
||||
// A core credential the vault did not answer for and no box was kept for is sealed by the controller itself until the
|
||||
// vault holds it (core_box.go, ADR 0396): the composition stands in a box for it, and the send carries the seal.
|
||||
sealedByController = boxes.byController
|
||||
}
|
||||
|
||||
// The certificate of a machine that was never certified: issued and kept, with the authority when there is none yet.
|
||||
for _, one := range d.StoodIn {
|
||||
if strings.HasPrefix(one, "the certificate and the authority of ") {
|
||||
if _, _, err := certificateFor(ctx, open, node); err != nil {
|
||||
return did, err
|
||||
return did, nil, err
|
||||
}
|
||||
did = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return did, nil
|
||||
return did, sealedByController, nil
|
||||
}
|
||||
|
||||
// makePair makes one pair credential by the controller's own read of it: the credential for the pair, or the provider's one
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/validate"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
@@ -92,6 +96,9 @@ func resolveNode(ctx context.Context, open *stores, node string) (catalogue.Reso
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
if err := in.refused[node]; err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
resolved, err := compose.Resolve(in.Input, node)
|
||||
if err != nil {
|
||||
if compose.IsUnresolvable(err) {
|
||||
@@ -123,6 +130,9 @@ func whoResolvesOn(ctx context.Context, open *stores, requirement string) (map[s
|
||||
on := map[string]bool{}
|
||||
refused := map[string]string{}
|
||||
for _, n := range in.sortedNodes() {
|
||||
if err := in.refused[n.Name]; err != nil {
|
||||
return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err)
|
||||
}
|
||||
resolved, err := compose.Resolve(in.Input, n.Name)
|
||||
switch {
|
||||
case compose.IsUnresolvable(err):
|
||||
@@ -160,10 +170,16 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if err := in.refused[node]; err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
d, problems, err := compose.ComposeIn(in.Input, node, mode)
|
||||
if err != nil {
|
||||
return sendable{}, nil, markUnresolvable(err)
|
||||
}
|
||||
if err := refusedBus(in, node); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
sayFound(ctx, d)
|
||||
out := sendableOf(d)
|
||||
if !withOrder {
|
||||
@@ -172,6 +188,25 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
|
||||
return out, problems, nil
|
||||
}
|
||||
|
||||
// refusedBus is why the node cannot be composed because it runs the bus and the bus's user list cannot be: no user has a
|
||||
// credential, so the list would refuse every connection in the mesh (busUserList). The controller's own composition refused that
|
||||
// machine with those words; no other machine is refused for it. nil when nothing refuses the node.
|
||||
func refusedBus(in *meshInput, node string) error {
|
||||
if in.busRefused == nil {
|
||||
return nil
|
||||
}
|
||||
resolved, err := compose.Resolve(in.Input, node)
|
||||
if err != nil {
|
||||
return nil // it does not compose for its own reasons, which are said
|
||||
}
|
||||
for _, m := range resolved.Plan.Modules {
|
||||
if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") {
|
||||
return in.busRefused
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markUnresolvable is an error of a node whose own set does not compose, as planFor marked it.
|
||||
func markUnresolvable(err error) error {
|
||||
if compose.IsUnresolvable(err) {
|
||||
@@ -191,15 +226,29 @@ func composeSend(ctx context.Context, open *stores, node string) (sendable, erro
|
||||
// composeSendValidated is composeSend and what the node-engine's validator refuses of the declaration it composed, for the
|
||||
// merge gate's two passes (composedAndValidated in its Allocating mode).
|
||||
func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) {
|
||||
// The core modules whose credentials the controller sealed itself because the vault was silent and no box was kept
|
||||
// (ADR 0396): the composition stands in a vault box for each, and a send carries the controller's own seal instead.
|
||||
var byController []string
|
||||
for attempt := 0; ; attempt++ {
|
||||
in, err := inputFor(ctx, open)
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if err := in.refused[node]; err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
d, problems, err := compose.ComposeIn(in.Input, node, compose.Foreseeing)
|
||||
if err != nil {
|
||||
return sendable{}, nil, markUnresolvable(err)
|
||||
}
|
||||
if err := refusedBus(in, node); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if len(byController) > 0 {
|
||||
if d, problems, err = sealedByTheController(in, node, d, byController); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
}
|
||||
left := stillForeseen(d)
|
||||
resolved, rerr := compose.Resolve(in.Input, node)
|
||||
if rerr != nil {
|
||||
@@ -227,9 +276,11 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda
|
||||
return sendable{}, nil, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+
|
||||
"that stands in for a value", node, strings.Join(left, "; "))
|
||||
}
|
||||
if _, err := perform(ctx, open, in, node, d); err != nil {
|
||||
_, sealed, err := performSend(ctx, open, in, node, d)
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
byController = sealed
|
||||
// What was made is in the stores now: the next read is a fresh one.
|
||||
forgetInput(ctx)
|
||||
}
|
||||
@@ -353,3 +404,66 @@ func handedPlans(in *meshInput, node string, resolved compose.Resolved) (map[str
|
||||
}
|
||||
return composed.Plans, nil
|
||||
}
|
||||
|
||||
// sealedByTheController is the declaration with the controller's own seal in place of the vault's box for each core module the
|
||||
// vault did not answer for and no box was kept for (core_box.go, ADR 0396): the file carries "sealed", as the controller's
|
||||
// composition sent it, until the vault holds the credential. What the composition stood in for the box is replaced and no longer
|
||||
// wanted; the validator is run again over the body.
|
||||
func sealedByTheController(in *meshInput, node string, d compose.Declared, modules []string) (compose.Declared, []string, error) {
|
||||
rendered, err := compose.Render(in.Input, node, compose.Foreseeing)
|
||||
if err != nil {
|
||||
return d, nil, err
|
||||
}
|
||||
stood, _ := compactBox(catalogue.ForeseenVaultBox)
|
||||
var wanted []compose.CoreBoxWanted
|
||||
var foreseen []string
|
||||
for _, w := range d.CoreBoxesWanted {
|
||||
if !slices.Contains(modules, w.Module) {
|
||||
wanted = append(wanted, w)
|
||||
continue
|
||||
}
|
||||
sealed := rendered.Rendering.Needed[w.Module][w.Name]
|
||||
if sealed == "" {
|
||||
return d, nil, fmt.Errorf("%s/%s on %s has no seal of the controller's to carry until the vault holds it", w.Module, w.Name, node)
|
||||
}
|
||||
path := in.Catalogue[w.Module].OwnSecrets[w.Name].Path
|
||||
for _, r := range d.Resources {
|
||||
box, isBox := r[catalogue.VaultSealedField]
|
||||
if !isBox || r["id"] != w.Module+"."+catalogue.NeedID(w.Name) || r["path"] != path {
|
||||
continue
|
||||
}
|
||||
if had, err := compactBox(box); err != nil || had != stood {
|
||||
continue
|
||||
}
|
||||
delete(r, catalogue.VaultSealedField)
|
||||
r["sealed"] = sealed
|
||||
}
|
||||
}
|
||||
d.CoreBoxesWanted = wanted
|
||||
for _, f := range d.Foreseen {
|
||||
module, _, _ := strings.Cut(f, "/")
|
||||
if !slices.Contains(modules, module) {
|
||||
foreseen = append(foreseen, f)
|
||||
}
|
||||
}
|
||||
d.Foreseen = foreseen
|
||||
body, err := d.Body()
|
||||
if err != nil {
|
||||
return d, nil, err
|
||||
}
|
||||
return d, validate.Declaration(body), nil
|
||||
}
|
||||
|
||||
// compactBox is a value as the compact JSON it marshals to, so a box is compared by what it says and not by the type that
|
||||
// holds it.
|
||||
func compactBox(v any) (string, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := json.Compact(&out, raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user