The provider-seal-key gate: on a node with two modules requiring the same provision (baserow
and letta both consuming postgres), sealedFor matched a need by provision NAME alone, so a
file's ${secret:X} placeholder took whichever consumer's sealed credential came last in
r.Needs -- the OTHER module's password. baserow was handed letta's password and could not
authenticate. The secrets:-map delivery path already guards this (For == m.Module, novox/hq
04-ISSUES/022); the ${secret:...} placeholder path did not. Added the same guard.
Also dedups the contributions file: when provider and consumer are co-located, grantsFor
enumerates the same-node consumer, so a consumer was emitted twice into the provider's
receives file (once full with its grant, once partial). The m.Contributes loop now skips a
(provision, module) the grants loop already carried; non-grant contributions (routes) still emit.
Regression test added: two consumers of one provision each get their own credential. Proven
end-to-end on a two-node lab install (mesh-lab assigned-two-node-db): baserow and letta on one
node, substrate on another, each authenticates with its own minted password.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
148 lines
5.6 KiB
Go
148 lines
5.6 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
)
|
|
|
|
// A credential and a configuration file meeting.
|
|
//
|
|
// **The gap this closes.** A granted credential arrives as a file whose entire content is the
|
|
// password. That is what a program reading a password file wants — and most programs do not read
|
|
// one. They read `KEY=value`, or a JSON document with the password at some path inside it, or a
|
|
// YAML file in a home directory. Before this, a module in that position could be handed the bare
|
|
// value or nothing, and both are useless.
|
|
//
|
|
// The mesh cannot compose the document, because it discarded the value (novox/hq ADR 0024). So
|
|
// the module supplies the document with a hole in it, the mesh delivers the value sealed beside
|
|
// it, and the host — the only thing that ever sees both — puts one into the other on the machine.
|
|
//
|
|
// The host has always been able to do this. Nothing filled the values in, so the hole could be
|
|
// written and never closed, and the host refused the file. That refusal was correct and the
|
|
// feature was unreachable.
|
|
|
|
// placeholder is what a module's file content says where a sealed value belongs: ${secret:name}.
|
|
//
|
|
// The same expression the host matches, written out again rather than shared: they are separate
|
|
// repositories and this is a wire format, like the shape of the declaration itself.
|
|
//
|
|
// **Nothing here can check that they agree, and the claim that something did was wrong.** A unit
|
|
// test in this repository can only assert what this repository already believes. What proves it is
|
|
// the lab, where a real host receives a real declaration and the file arrives filled — and where
|
|
// the two expressions disagreeing shows up as a placeholder written through to a machine.
|
|
var placeholder = regexp.MustCompile(`\$\{secret:([a-z0-9][a-z0-9-]*)\}`)
|
|
|
|
// secretsUsed are the names a file's content asks for, in the order they first appear.
|
|
func secretsUsed(content string) []string {
|
|
var used []string
|
|
seen := map[string]bool{}
|
|
for _, m := range placeholder.FindAllStringSubmatch(content, -1) {
|
|
if !seen[m[1]] {
|
|
seen[m[1]] = true
|
|
used = append(used, m[1])
|
|
}
|
|
}
|
|
return used
|
|
}
|
|
|
|
// sealedFor is every credential a module may name from inside one of its own files.
|
|
//
|
|
// **Exactly what it already declared, and nothing else.** A module reaches its own secrets and the
|
|
// credentials it was granted for what it requires — both written down in its own manifest. It
|
|
// cannot name another module's, which is not an oversight: two modules on one machine are as
|
|
// separate as two on different machines, and letting one read the other's credential by guessing a
|
|
// name would end that, to save writing a file.
|
|
func sealedFor(m Manifest, needs []Needed, with Rendering) (map[string]string, error) {
|
|
sealed := map[string]string{}
|
|
for name := range m.OwnSecrets {
|
|
if value := with.Needed[m.Module][name]; value != "" {
|
|
sealed[name] = value
|
|
}
|
|
}
|
|
for _, to := range sortedKeys(m.Secrets) {
|
|
if _, taken := sealed[to]; taken {
|
|
// A module whose own secret and whose requirement share a name. Refused rather than
|
|
// settled by precedence: whichever won, the manifest would read as though the other
|
|
// had, and the file would hold the credential for the wrong thing while every check
|
|
// passed.
|
|
return nil, fmt.Errorf(
|
|
"%s has a secret of its own called %q and also requires %q, so a file saying "+
|
|
"${secret:%s} could mean either — rename one of them", m.Module, to, to, to)
|
|
}
|
|
for i := range needs {
|
|
// `For == m.Module`, not name alone: on a node with two modules requiring the same
|
|
// provision, both appear in `needs`, and matching by name would fill ${secret:X} with
|
|
// whichever came last — the other module's credential (novox/hq 04-ISSUES/022). The
|
|
// `secrets:`-map path already guards this way; the ${secret:…} placeholder path did not.
|
|
if needs[i].Name == to && needs[i].For == m.Module && needs[i].Sealed != "" {
|
|
sealed[to] = needs[i].Sealed
|
|
}
|
|
}
|
|
}
|
|
return sealed, nil
|
|
}
|
|
|
|
// intoFile gives a file the sealed values its content asks for.
|
|
//
|
|
// A name the module never declared is refused here rather than on the machine. The host would
|
|
// refuse it too — but it would do so having already been handed a declaration, which reads as the
|
|
// mesh sending something broken, and the name it could not find is a manifest's typo.
|
|
func intoFile(resource map[string]any, sealed map[string]string, module string) error {
|
|
if fmt.Sprint(resource["type"]) != "file" {
|
|
return nil
|
|
}
|
|
content, ok := resource["content"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
used := secretsUsed(content)
|
|
if len(used) == 0 {
|
|
return nil
|
|
}
|
|
into := map[string]any{}
|
|
for _, name := range used {
|
|
value := sealed[name]
|
|
if value == "" {
|
|
return fmt.Errorf(
|
|
"%s has a file that says ${secret:%s}, and %s has no secret of its own by that "+
|
|
"name and requires nothing called that either. A file may name %s",
|
|
module, name, module, namesOr(sealed))
|
|
}
|
|
into[name] = value
|
|
}
|
|
resource["secrets"] = into
|
|
return nil
|
|
}
|
|
|
|
// namesOr says what a module could have written, because the answer to "that name is wrong" is
|
|
// almost always one of two or three right ones.
|
|
func namesOr(sealed map[string]string) string {
|
|
if len(sealed) == 0 {
|
|
return "nothing — it has no secrets of its own and requires nothing that grants one"
|
|
}
|
|
var names []string
|
|
for name := range sealed {
|
|
names = append(names, fmt.Sprintf("%q", name))
|
|
}
|
|
sort.Strings(names)
|
|
return join(names)
|
|
}
|
|
|
|
func join(names []string) string {
|
|
switch len(names) {
|
|
case 1:
|
|
return names[0]
|
|
case 2:
|
|
return names[0] + " or " + names[1]
|
|
}
|
|
out := ""
|
|
for i, n := range names[:len(names)-1] {
|
|
if i > 0 {
|
|
out += ", "
|
|
}
|
|
out += n
|
|
}
|
|
return out + " or " + names[len(names)-1]
|
|
}
|