- A placeholder is `${secret:<name>}`, the name the module calls the credential (catalogue.SecretLocal): the local
name where it keeps several values of one provision, else the provision. The `<provision>.<local>` form goes.
- The engine finds the one key under `<node>.<module>.` of mesh-provisioner's `provisions` going by that name. That is
what node.<node> may read, by a watch to its caught-up mark; plans are not read.
- The reference's ID must be a credential name of that provision (provisions.VaultName). The vault's answer must carry
exactly that name and generation. Anything else refuses the write, naming both.
Pins mesh-sdk 66ff33d. Tests: rows in TestAWriteAndAnApplyOfTheSameFileSerialise (a local name, a name no provision
goes by, a reference to no credential of the provision, a mismatched answer).
169 lines
6.9 KiB
Go
169 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/provisions"
|
|
)
|
|
|
|
// provisionSecrets fills a provision's value into a file the provisioner's client hands the node-engine to write
|
|
// (novox/hq ADR 0570 clause 6): the client fills the file but for each `${secret:<provision>}`, and the node-engine
|
|
// fills those at the write, so the client never holds the value. A whole `secrets` file of a handed consumer is the
|
|
// placeholder alone.
|
|
//
|
|
// A placeholder names the credential as the module calls it, `${secret:<name>}` (catalogue.SecretLocal, ADR 0094): the
|
|
// local name where the module keeps several values of one provision, else the provision. For each, it finds the one
|
|
// provision key `<node>.<consumer>.<provision>[.<local>]` of the seat mesh-provisioner's state `provisions` (clause 1)
|
|
// going by that name, which must be `provided` and carry the vault's reference; asks the vault for that pair
|
|
// credential sealed to this machine's key (the verb `own`, ADR 0349 §11 with the node-engine standing for the client,
|
|
// ADR 0550 §5); and opens it with this machine's sealing key. Anything short of that refuses the write, naming the
|
|
// provision and why: a file written with a placeholder left in it, or with no value, is a module that starts and cannot
|
|
// connect.
|
|
type provisionSecrets struct {
|
|
node string
|
|
// list is every key of mesh-provisioner's `provisions` under prefix (`<node>.<consumer>.`), by key: what this
|
|
// engine may read, its own node's (`<node>.>`).
|
|
list func(ctx context.Context, prefix string) (map[string][]byte, error)
|
|
// own asks the vault's verb `own` in this engine's name.
|
|
own func(ctx context.Context, args map[string]any) (json.RawMessage, error)
|
|
// open opens a value sealed to this machine.
|
|
open func(sealed string) ([]byte, error)
|
|
}
|
|
|
|
// secretPlaceholder is `${secret:<name>}`.
|
|
var secretPlaceholder = regexp.MustCompile(`\$\{secret:([A-Za-z0-9_-]+)\}`)
|
|
|
|
// fill is content with each provision placeholder of module's filled, and whether any was.
|
|
func (p provisionSecrets) fill(ctx context.Context, module string, content []byte) ([]byte, bool, error) {
|
|
found := secretPlaceholder.FindAllSubmatch(content, -1)
|
|
if len(found) == 0 {
|
|
return content, false, nil
|
|
}
|
|
if module == "" {
|
|
return nil, false, fmt.Errorf("the file names %d provision secret(s) and belongs to no module, so whose they are is not known", len(found))
|
|
}
|
|
if p.list == nil || p.own == nil || p.open == nil {
|
|
return nil, false, fmt.Errorf("%s's provisions cannot be read on this machine: the node-engine is not linked to the mesh", module)
|
|
}
|
|
prefix := p.node + "." + module + "."
|
|
keys, err := p.list(ctx, prefix)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("%s's provisions cannot be read: %w", module, err)
|
|
}
|
|
out := string(content)
|
|
done := map[string]bool{}
|
|
for _, f := range found {
|
|
whole, name := string(f[0]), string(f[1])
|
|
if done[whole] {
|
|
continue
|
|
}
|
|
done[whole] = true
|
|
pr, key, err := byName(keys, name)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("%s's secret %q: %w", module, name, err)
|
|
}
|
|
value, err := p.value(ctx, key, pr)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
out = strings.ReplaceAll(out, whole, string(value))
|
|
}
|
|
return []byte(out), true, nil
|
|
}
|
|
|
|
// byName is the one provision of keys the module calls name (catalogue.SecretLocal), and its key.
|
|
func byName(keys map[string][]byte, name string) (provisions.Provision, string, error) {
|
|
var hit []string
|
|
var got provisions.Provision
|
|
for key, raw := range keys {
|
|
pr, err := provisions.Decode(raw)
|
|
if err != nil {
|
|
return provisions.Provision{}, "", fmt.Errorf("the provision %s: %w", key, err)
|
|
}
|
|
if catalogue.SecretLocal(pr.Provision, pr.Local) == name {
|
|
hit, got = append(hit, key), pr
|
|
}
|
|
}
|
|
switch len(hit) {
|
|
case 0:
|
|
return provisions.Provision{}, "", fmt.Errorf("no provision of %s goes by that name: nothing asks for it", provisions.ProvisionsRead)
|
|
case 1:
|
|
return got, hit[0], nil
|
|
}
|
|
sort.Strings(hit)
|
|
return provisions.Provision{}, "", fmt.Errorf("the provisions %s all go by that name", strings.Join(hit, ", "))
|
|
}
|
|
|
|
// value is one provision's value on this node, opened.
|
|
func (p provisionSecrets) value(ctx context.Context, key string, pr provisions.Provision) ([]byte, error) {
|
|
if pr.State != provisions.Provided || pr.Reference == nil {
|
|
why := pr.State
|
|
if pr.Why != "" {
|
|
why += ": " + pr.Why
|
|
}
|
|
if pr.State == provisions.Provided {
|
|
why = "provided with no reference to the vault's value"
|
|
}
|
|
return nil, fmt.Errorf("the provision %s is %s, so its value is not written yet", key, why)
|
|
}
|
|
// The reference names a credential of this provision, as the vault names it (provisions.VaultName).
|
|
ours := false
|
|
for _, form := range []string{provisions.FormPair, provisions.FormShared, provisions.FormIssued} {
|
|
ours = ours || provisions.VaultName(form, pr) == pr.Reference.ID
|
|
}
|
|
if !ours {
|
|
return nil, fmt.Errorf("the provision %s names the vault's %s, which is no credential of it", key, pr.Reference.ID)
|
|
}
|
|
module, provision, local := pr.Consumer.Module, pr.Provision, pr.Local
|
|
type asked struct {
|
|
Role string `json:"role"`
|
|
Provision string `json:"provision"`
|
|
Consumer string `json:"consumer"`
|
|
ConsumerModule string `json:"consumer_module"`
|
|
Provider string `json:"provider"`
|
|
Local string `json:"local,omitempty"`
|
|
}
|
|
ask := asked{Role: "consumer", Provision: provision, Consumer: p.node, ConsumerModule: module, Provider: pr.Provider.Node, Local: local}
|
|
answer, err := p.own(ctx, map[string]any{"credentials": []asked{ask}})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the vault's value of %s: %w", key, err)
|
|
}
|
|
var a struct {
|
|
Node string `json:"node"`
|
|
Credentials []struct {
|
|
asked
|
|
Name string `json:"name"`
|
|
Generation int `json:"generation"`
|
|
Sealed json.RawMessage `json:"sealed"`
|
|
} `json:"credentials"`
|
|
}
|
|
if err := json.Unmarshal(answer, &a); err != nil {
|
|
return nil, fmt.Errorf("the vault's answer for %s is not one: %w", key, err)
|
|
}
|
|
if a.Node != p.node {
|
|
return nil, fmt.Errorf("the vault answered %s's credentials for %s on %s", a.Node, key, p.node)
|
|
}
|
|
for _, c := range a.Credentials {
|
|
if c.asked != ask {
|
|
continue
|
|
}
|
|
// The value the provision names, by the vault's credential name and its generation (clause 6): another is refused.
|
|
if c.Name != pr.Reference.ID || c.Generation != pr.Reference.Generation {
|
|
return nil, fmt.Errorf("the vault answered %s generation %d for %s, and the provision names %s generation %d: "+
|
|
"nothing is written until they agree", c.Name, c.Generation, key, pr.Reference.ID, pr.Reference.Generation)
|
|
}
|
|
value, err := p.open(string(c.Sealed))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the vault's value of %s does not open on this machine: %w", key, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
return nil, fmt.Errorf("the vault answered no value of %s for this machine", key)
|
|
}
|