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 }