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.
|
// 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) {
|
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
|
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).
|
// 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 {
|
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.
|
// down. The error is the one planFor met: the machine that needs it, and why none could be made.
|
||||||
for _, p := range in.wouldMake {
|
for _, p := range in.wouldMake {
|
||||||
if err := makePair(ctx, inv, p); err != nil {
|
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)
|
p.ConsumerModule, p.Consumer, p.Name, p.Provider, err)
|
||||||
}
|
}
|
||||||
did = true
|
did = true
|
||||||
@@ -82,13 +89,13 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
|||||||
if len(d.ForeseenPorts) > 0 {
|
if len(d.ForeseenPorts) > 0 {
|
||||||
shelf, err := inv.Catalogue(ctx)
|
shelf, err := inv.Catalogue(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return did, err
|
return did, nil, err
|
||||||
}
|
}
|
||||||
for _, one := range d.ForeseenPorts {
|
for _, one := range d.ForeseenPorts {
|
||||||
module, port, ok := strings.Cut(one, ":")
|
module, port, ok := strings.Cut(one, ":")
|
||||||
wanted, perr := strconv.Atoi(port)
|
wanted, perr := strconv.Atoi(port)
|
||||||
if !ok || perr != nil {
|
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
|
fixed := false
|
||||||
for _, l := range nodeShelf(in, shelf, node)[module].Listens {
|
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 {
|
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
|
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
|
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 {
|
if _, err := inv.SecretForModule(ctx, node, module, name); err != nil {
|
||||||
return did, err
|
return did, nil, err
|
||||||
}
|
}
|
||||||
did = true
|
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 {
|
if ok && n.Stated && n.Contract >= compose.VaultBoxContract {
|
||||||
resolved, err := compose.Resolve(in.Input, node)
|
resolved, err := compose.Resolve(in.Input, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return did, err
|
return did, nil, err
|
||||||
}
|
}
|
||||||
hands := n.Stated && n.Contract >= compose.HandedContract
|
hands := n.Stated && n.Contract >= compose.HandedContract
|
||||||
handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)}
|
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)
|
return handing.Handing(m)
|
||||||
}, Allocating)
|
}, Allocating)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return did, err
|
return did, nil, err
|
||||||
}
|
}
|
||||||
if len(boxes.keep) > 0 {
|
if len(boxes.keep) > 0 {
|
||||||
did = true
|
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
|
// A core credential the vault did not answer for and no box was kept for is sealed by the controller itself until the
|
||||||
// (core_box.go, ADR 0396); the composition over an Input has no such fallback, so the send is refused, said.
|
// vault holds it (core_box.go, ADR 0396): the composition stands in a box for it, and the send carries the seal.
|
||||||
if len(boxes.byController) > 0 {
|
sealedByController = boxes.byController
|
||||||
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, ", "))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The certificate of a machine that was never certified: issued and kept, with the authority when there is none yet.
|
// 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 {
|
for _, one := range d.StoodIn {
|
||||||
if strings.HasPrefix(one, "the certificate and the authority of ") {
|
if strings.HasPrefix(one, "the certificate and the authority of ") {
|
||||||
if _, _, err := certificateFor(ctx, open, node); err != nil {
|
if _, _, err := certificateFor(ctx, open, node); err != nil {
|
||||||
return did, err
|
return did, nil, err
|
||||||
}
|
}
|
||||||
did = true
|
did = true
|
||||||
break
|
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
|
// 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/validate"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"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 {
|
if err != nil {
|
||||||
return catalogue.Resolution{}, nil, err
|
return catalogue.Resolution{}, nil, err
|
||||||
}
|
}
|
||||||
|
if err := in.refused[node]; err != nil {
|
||||||
|
return catalogue.Resolution{}, nil, err
|
||||||
|
}
|
||||||
resolved, err := compose.Resolve(in.Input, node)
|
resolved, err := compose.Resolve(in.Input, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if compose.IsUnresolvable(err) {
|
if compose.IsUnresolvable(err) {
|
||||||
@@ -123,6 +130,9 @@ func whoResolvesOn(ctx context.Context, open *stores, requirement string) (map[s
|
|||||||
on := map[string]bool{}
|
on := map[string]bool{}
|
||||||
refused := map[string]string{}
|
refused := map[string]string{}
|
||||||
for _, n := range in.sortedNodes() {
|
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)
|
resolved, err := compose.Resolve(in.Input, n.Name)
|
||||||
switch {
|
switch {
|
||||||
case compose.IsUnresolvable(err):
|
case compose.IsUnresolvable(err):
|
||||||
@@ -160,10 +170,16 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return sendable{}, nil, err
|
return sendable{}, nil, err
|
||||||
}
|
}
|
||||||
|
if err := in.refused[node]; err != nil {
|
||||||
|
return sendable{}, nil, err
|
||||||
|
}
|
||||||
d, problems, err := compose.ComposeIn(in.Input, node, mode)
|
d, problems, err := compose.ComposeIn(in.Input, node, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendable{}, nil, markUnresolvable(err)
|
return sendable{}, nil, markUnresolvable(err)
|
||||||
}
|
}
|
||||||
|
if err := refusedBus(in, node); err != nil {
|
||||||
|
return sendable{}, nil, err
|
||||||
|
}
|
||||||
sayFound(ctx, d)
|
sayFound(ctx, d)
|
||||||
out := sendableOf(d)
|
out := sendableOf(d)
|
||||||
if !withOrder {
|
if !withOrder {
|
||||||
@@ -172,6 +188,25 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos
|
|||||||
return out, problems, nil
|
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.
|
// markUnresolvable is an error of a node whose own set does not compose, as planFor marked it.
|
||||||
func markUnresolvable(err error) error {
|
func markUnresolvable(err error) error {
|
||||||
if compose.IsUnresolvable(err) {
|
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
|
// 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).
|
// merge gate's two passes (composedAndValidated in its Allocating mode).
|
||||||
func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) {
|
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++ {
|
for attempt := 0; ; attempt++ {
|
||||||
in, err := inputFor(ctx, open)
|
in, err := inputFor(ctx, open)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendable{}, nil, err
|
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)
|
d, problems, err := compose.ComposeIn(in.Input, node, compose.Foreseeing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendable{}, nil, markUnresolvable(err)
|
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)
|
left := stillForeseen(d)
|
||||||
resolved, rerr := compose.Resolve(in.Input, node)
|
resolved, rerr := compose.Resolve(in.Input, node)
|
||||||
if rerr != nil {
|
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 "+
|
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, "; "))
|
"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
|
return sendable{}, nil, err
|
||||||
}
|
}
|
||||||
|
byController = sealed
|
||||||
// What was made is in the stores now: the next read is a fresh one.
|
// What was made is in the stores now: the next read is a fresh one.
|
||||||
forgetInput(ctx)
|
forgetInput(ctx)
|
||||||
}
|
}
|
||||||
@@ -353,3 +404,66 @@ func handedPlans(in *meshInput, node string, resolved compose.Resolved) (map[str
|
|||||||
}
|
}
|
||||||
return composed.Plans, nil
|
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