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. The two // repositories agree on a wire format, and a format read on both sides is exactly the thing that // must not be quietly changed on one of them; a test asserts they still agree. 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 { if needs[i].Name == to && 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] }