package provider import ( "context" "encoding/json" "errors" "fmt" "io/fs" "os" ) // The provider's consumers' credentials, from the file its machine's provisioner client fills (novox/hq ADR 0373, // issue 679). // // **The provider never asks the vault.** A provider in the tool runner has no account of its own, and the vault // hands a secret only to a node's provisioner client or to a module under an account of its own (ADR 0349 clause // 11, ADR 0370 clause 2). So the provider's template names `${consumers:}`, and its machine's client // fills that file with one entry per consumer bound to it for that provision, each credential asked of the vault in // the client's own name; the file is the provider's alone (mode 0600, owned). ConsumersFile is that file as the // helper's Vault: it reads the file on each ask, so a consumer that binds or a credential the vault remakes reaches // the provider at its next pass. // ConsumerCredential is one entry of the file: the consumer, the vault's name of its credential, and its value. type ConsumerCredential struct { // Consumer and Module are the consumer's machine and module, Local its local name for the provision, if any. Consumer string `json:"consumer"` Module string `json:"module"` Local string `json:"local,omitempty"` // Credential is the vault's name of the consumer's credential, PairName's. Credential string `json:"credential"` // Value is the credential itself. Value string `json:"value"` } // ConsumersFile answers a credential from the files the provider machine's client fills: one per provision whose // template names `${consumers:}`, each a JSON list of ConsumerCredential. type ConsumersFile struct { Paths []string } // Value is the credential named name, from the first file carrying it. A file not there yet is passed over; one not // in the client's form is an error, never read as no consumers. func (f ConsumersFile) Value(_ context.Context, name string) ([]byte, error) { var missing []string for _, at := range f.Paths { raw, err := os.ReadFile(at) if errors.Is(err, fs.ErrNotExist) { missing = append(missing, at) continue } if err != nil { return nil, fmt.Errorf("the consumers' file %s: %w", at, err) } var entries []ConsumerCredential if err := json.Unmarshal(raw, &entries); err != nil { return nil, fmt.Errorf("the consumers' file %s is not the provisioner client's list of consumers", at) } for _, e := range entries { if e.Credential == name { if e.Value == "" { return nil, fmt.Errorf("the consumers' file %s carries %s with no value", at, name) } return []byte(e.Value), nil } } } if len(missing) == len(f.Paths) { return nil, fmt.Errorf("%s: no consumers' file is filled yet (%v)", name, f.Paths) } return nil, fmt.Errorf("%s is in no consumers' file: the vault holds none for it yet, or its machine's client "+ "has not filled it", name) }