116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
package resolution
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// secretPlaces is every place on this node a secret is kept, as the placed manifests say (novox/hq ADR 0310
|
|
// §5): the directory of every module's own secret, every credential file a module is given and every grant
|
|
// directory a provider is handed its consumers' credentials in, the place a module keeps the operator-sealed
|
|
// secrets, and every directory of the module that provides `secret` — the vault — wherever the node places it,
|
|
// a directory declared without a path included. Sorted, each once, and a place under another dropped, so the
|
|
// list says each refusal once.
|
|
//
|
|
// **Failure is loud.** A place still holding a placeholder, or not absolute, fails the composition with the
|
|
// module named: a list that silently shrank would be a refusal the decision promises and nothing does.
|
|
func resolutionSecretPlaces(r mcat.Resolution, with mcat.Rendering) ([]string, error) {
|
|
seen := map[string]bool{}
|
|
var problems []string
|
|
add := func(module, what, path string) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(path, "/") || strings.Contains(path, "${") {
|
|
problems = append(problems, fmt.Sprintf("%s's %s is %q, which is not a place on %s", module, what, path, r.Node))
|
|
return
|
|
}
|
|
path = filepath.Clean(path)
|
|
if path == "/" {
|
|
problems = append(problems, fmt.Sprintf("%s's %s is the root directory", module, what))
|
|
return
|
|
}
|
|
seen[path] = true
|
|
}
|
|
for _, m := range r.Modules {
|
|
for name, own := range m.OwnSecrets {
|
|
add(m.Module, "own secret "+name, own.Path)
|
|
if strings.HasPrefix(own.Path, "/") {
|
|
add(m.Module, "own secret "+name+"'s directory", filepath.Dir(own.Path))
|
|
}
|
|
}
|
|
for _, to := range m.SecretRequirements() {
|
|
for _, f := range m.SecretFiles(to) {
|
|
add(m.Module, "credential file for "+to, f.Path)
|
|
}
|
|
}
|
|
for to, where := range m.Grants {
|
|
add(m.Module, "grant directory for "+to, where)
|
|
}
|
|
add(m.Module, "place for the operator-sealed secrets (keeps)", m.Keeps)
|
|
if providesSecret(m) {
|
|
dirs := dirsFor(m, with)
|
|
for _, id := range sortedKeys(dirs) {
|
|
add(m.Module, "directory "+id, dirs[id])
|
|
}
|
|
}
|
|
}
|
|
if len(problems) > 0 {
|
|
sort.Strings(problems)
|
|
return nil, fmt.Errorf("the holder of %s on %s cannot be told where a secret is kept, so it would read "+
|
|
"one (novox/hq ADR 0310 §5): %s", mcat.ReadsSeat, r.Node, strings.Join(problems, "; "))
|
|
}
|
|
all := make([]string, 0, len(seen))
|
|
for p := range seen {
|
|
all = append(all, p)
|
|
}
|
|
sort.Strings(all)
|
|
var out []string
|
|
for _, p := range all {
|
|
if n := len(out); n > 0 && strings.HasPrefix(p, out[n-1]+"/") {
|
|
continue
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func providesSecret(m mcat.Manifest) bool {
|
|
for _, p := range m.Provides {
|
|
if p.Name == "secret" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// readsWords is what the holder of node-reads is given beside its own words: the node's secret places.
|
|
func resolutionReadsWords(r mcat.Resolution, m mcat.Manifest, words map[string]string, with mcat.Rendering) (map[string]string, error) {
|
|
if !m.ClaimsSeat(mcat.ReadsSeat) {
|
|
return words, nil
|
|
}
|
|
places, err := resolutionSecretPlaces(r, with)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := json.Marshal(places)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if words == nil {
|
|
words = map[string]string{}
|
|
}
|
|
if _, has := words[mcat.ReadsSecretPlaces]; has {
|
|
return nil, fmt.Errorf("%s gives its own %s; the node's secret places are the mesh's to say (novox/hq ADR 0310 §5)",
|
|
m.Module, mcat.ReadsSecretPlaces)
|
|
}
|
|
words[mcat.ReadsSecretPlaces] = string(body)
|
|
return words, nil
|
|
}
|