package catalogue import ( "fmt" "regexp" "slices" ) // A provider's consumers' credentials, in a file of the provider's own (novox/hq ADR 0373, issue 679). // // A provider in the tool runner may not ask the vault (ADR 0349 clause 11, ADR 0370 clause 2), so its template names // `${consumers:}` and its machine's provisioner client fills it: one entry per consumer bound to the // provider for that provision, each carrying the credential the vault keeps for the pair. Only the client fills it, // so the placeholder is a provider's alone, for a provision it provides, in a file only the provider reads (mode 0600 // or 0400, owned at runtime, so a change on disk is written back); and a module not handed to the client on a machine // is refused there, never written the placeholder's text, nor an empty list the provider would read as no consumers. // ConsumersNamespace is a provider's consumers' credentials' placeholder namespace, `${consumers:}`. const ConsumersNamespace = "consumers" // ofConsumers is where a provider places its consumers' credentials. Loose inside the braces, so a misspelt name is // refused rather than passed over. var ofConsumers = regexp.MustCompile(`\$\{consumers:([^}]*)\}`) // consumersPlaceholderProblems is what is wrong with one resource's ${consumers:…}: placed only in a file's content, // for a provision the module provides, in a file only the provider reads and the client writes back. func consumersPlaceholderProblems(m Manifest, r map[string]any) []string { var problems []string for _, field := range sortedKeys(r) { v, ok := r[field].(string) if !ok { continue } for _, f := range ofConsumers.FindAllStringSubmatch(v, -1) { switch { case field != "content" || fmt.Sprint(r["type"]) != "file": problems = append(problems, fmt.Sprintf("%s's resource %v names %s in its %s; a provider's consumers "+ "are placed only in a file's content (novox/hq ADR 0373)", m.Module, r["id"], f[0], field)) continue case !slices.ContainsFunc(m.Provides, func(o Offer) bool { return o.Name == f[1] }): problems = append(problems, fmt.Sprintf("%s's resource %v names %s, and %s does not provide %q: only a "+ "provider's template holds its consumers' credentials (novox/hq ADR 0373)", m.Module, r["id"], f[0], m.Module, f[1])) } if mode := fmt.Sprint(r["mode"]); mode != "0600" && mode != "0400" { problems = append(problems, fmt.Sprintf("%s's resource %v holds its consumers' credentials (%s) with "+ "mode %q: the provider alone reads it, 0600 or 0400 (novox/hq ADR 0373)", m.Module, r["id"], f[0], r["mode"])) } if owner := ownershipOf(r); owner != RuntimeOwned { problems = append(problems, fmt.Sprintf("%s's resource %v holds its consumers' credentials (%s) and is "+ "%s at runtime: it is owned, written back whenever it differs (novox/hq ADR 0373)", m.Module, r["id"], f[0], owner)) } } } return problems } // consumersHandedOnly says what a file naming ${consumers:…} is where it would be written the old way: the provisioner's // client alone fills it, so on a machine that does not hand the module it is **left out** of the declaration, never // written as that text or as an empty list the provider would read as no consumers, and never the reason the module is // refused: a provider not handed there keeps working as it did, from its grant files (novox/hq ADR 0373, issue 780). // A moved module judged from its repository's tree (WithTemplatesFrom: `module check`, the merge gate), on a machine // that would hand it, has no templates archive to hand and is judged as it composes; there the placeholder is judged // and taken out of the text, since what is composed so is never sent (judged answers whether the file is so judged). func consumersHandedOnly(m Manifest, r map[string]any, with Rendering) (judged, leftOut bool) { content, _ := r["content"].(string) switch { case ofConsumers.FindString(content) == "": return false, false case m.Moved && m.Handed == nil && with.Hands && with.Provisioner != "": return true, false } return false, true } // consumersLeftOutLine is the plan's one line for a file left out because the module is not handed. func consumersLeftOutLine(m Manifest, r map[string]any) string { return fmt.Sprintf("%s's %v names ${consumers:…}, which the provisioner's client alone fills: left out, handed only "+ "(novox/hq ADR 0373)", m.Module, r["id"]) } // consumersNotHanded is consumersHandedOnly as a refusal, for a caller that has no way to leave the file out. func consumersNotHanded(m Manifest, r map[string]any, with Rendering) (judged bool, err error) { judged, left := consumersHandedOnly(m, r, with) if left { return false, fmt.Errorf("%s's resource %v names %s, which the provisioner's client alone fills, and %s is not "+ "handed to it on this machine: it would be written as that text, or empty, and the provider would read no "+ "consumers (novox/hq ADR 0373)", m.Module, r["id"], ofConsumers.FindString(fmt.Sprint(r["content"])), m.Module) } return judged, nil } // consumersOfTemplate is what is wrong with a handed file's template naming ${consumers:…}, judged on the file's // resource as if its text were inline. func consumersOfTemplate(m Manifest, id, text string) []string { for _, r := range m.Resources { if fmt.Sprint(r["id"]) != id { continue } withText := map[string]any{} for k, v := range r { withText[k] = v } withText["content"] = text return consumersPlaceholderProblems(m, withText) } return nil }