Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/pair.go
T

815 lines
32 KiB
Go

package main
// The credential a provider shares with its consumers (novox/hq ADR 0349 clause 8; ADR 0158 §4–§6, §8; design 57's
// V5, issue 677): one value per provider assignment and provision, `provision/<provider node>/<provision>`, minted
// and remade by the vault for all its holders together when a consumer binds or unbinds, or when it is rotated.
//
// **Where the vault sees a bind.** A recorded need names no provider (issue 677's note 7). The controller names, in
// the plan of each module it handed to the provisioner, each provision the module requires with the provider
// resolution chose (issue 760): `provisions` in `<node>.<module>` of the assignments mirror, the plan the module's
// client records its needs from. A consumer binds when such an entry appears and unbinds when it goes, or when its
// assignment goes. The vault reads every plan on each pass (once a minute, and when it starts).
//
// **Every holder can get it, or nothing is made** (the lead's rule on issue 677, note 10). The holders are the
// provider on its machine and every consumer bound to it. Only a module handed to the provisioner gets a value from
// the vault; one still filled the old way gets the controller's (ADR 0349 clause 17). So the vault mints or remakes
// a provider's value only when the provider's assignment is handed to the provisioner, and no consumer the vault
// knows to hold it is still filled the old way: a consumer whose pair row the move brought in (V3) and whose
// assignment is still there with no plan. Otherwise it makes nothing, and says why on each pass that changes what
// it says. A consumer filled the old way that bound after the move names no provider on the bus at all; it is the
// provider's move (design 57 B2, B3) that ends that case.
//
// **What the move brought in is adopted, and nothing is rotated early.** The move (V3) copied each pair row as
// `pair/<provider>/<provision>/<consumer>/<module>[/<local>]`. Rows a provider shares (ADR 0158: its own secret and
// every consumer's row made in one act) carry one generation stamp; when every row of a provider and provision
// carries the same stamp and the same value, the vault keeps that value as the provision's one credential, with its
// origin, and remakes nothing. Rows of a provider with a login per consumer (ADR 0048) carry no stamp and differ
// per pair: each consumer keeps a value of its own for ever (ADR 0158 §11, issue 677's note 12). The vault never
// makes them one shared value; a rotation (`rotate` of `provision/<provider>/<provision>`) remakes each consumer's
// own apart, and each consumer's machine is handed only its own.
//
// **A provider the vault holds no row of gets its first value in its offer's form** (issue 768). Each plan entry
// names the form the provider's offer declares (`credential-form`, from the offer's `credential` in the provider's
// manifest): `shared`, one value for every holder (ADR 0158), made as `provision/<provider>/<provision>`; `pair`, a
// login of its own for each consumer (ADR 0048, ADR 0158 §11), made as each consumer's pair row; `issued` by the
// provider's software (ADR 0305) or `given` by a person (ADR 0337), which the vault never makes. An entry naming no
// form (a controller from before), plans that disagree, or a form the vault does not know: nothing is made, rather
// than a guess. A provider the vault already holds rows of is unchanged by the form: it is adopted or kept per pair
// as above.
//
// **An accepted or issued value is never remade** (ADR 0158 §8, ADR 0305 §9): the vault records who holds it and
// hands it to them, and leaves the value alone. A provision whose plans name a credential per holder (issue 757,
// an entry with a name) has no one shared value and is not the vault's to make here.
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// Mirror is the controller's assignments mirror as the vault reads it: every key, and one key's value.
type Mirror struct {
Keys func() ([]string, error)
Read AssignmentReader
}
// Shared is what the vault last made, adopted or recorded the provision's one credential for: its holders and the
// generation they hold. Never a value.
type Shared struct {
Name string `json:"name"`
Provider string `json:"provider"`
Provision string `json:"provision"`
ProviderModule string `json:"provider-module,omitempty"`
// Consumers are the binds holding it: `<node>/<module>`, and `/<local>` when bound under a local name.
Consumers []string `json:"consumers"`
Generation int `json:"generation"`
At time.Time `json:"at"`
// Adopted is the controller's generation stamp of the pair rows the value was adopted from, when it was.
Adopted string `json:"adopted,omitempty"`
}
// holderNodes are the machines holding it: the provider's and every consumer's.
func (s Shared) holderNodes() []string {
out := []string{s.Provider}
for _, c := range s.Consumers {
out = append(out, strings.SplitN(c, "/", 2)[0])
}
return out
}
func (s *Store) sharedPath(name string) string {
return filepath.Join(s.dir, "shared", url.PathEscape(name)+".json")
}
// ReadShared is the record of who holds the provision's one credential name, and whether there is one.
func (s *Store) ReadShared(name string) (Shared, bool, error) {
if err := validName(name); err != nil {
return Shared{}, false, err
}
raw, err := os.ReadFile(s.sharedPath(name))
if errors.Is(err, os.ErrNotExist) {
return Shared{}, false, nil
}
if err != nil {
return Shared{}, false, err
}
var sh Shared
if err := json.Unmarshal(raw, &sh); err != nil {
return Shared{}, false, fmt.Errorf("the record of who holds %s is not readable: %w", name, err)
}
return sh, true, nil
}
// WriteShared keeps the record of who holds a provision's one credential.
func (s *Store) WriteShared(sh Shared) error {
if err := validName(sh.Name); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(s.dir, "shared"), 0o700); err != nil {
return err
}
sort.Strings(sh.Consumers)
raw, err := json.MarshalIndent(sh, "", " ")
if err != nil {
return err
}
return writeWhole(s.sharedPath(sh.Name), append(raw, '\n'))
}
// pairKey is one provider assignment and provision.
type pairKey struct{ provider, provision string }
func (k pairKey) name() string { return "provision/" + k.provider + "/" + k.provision }
// binds is what the plans say of one provider and provision: its module, who binds it, whether a plan names a
// credential per holder for it, and the credential forms the plans name for it ("" for an entry naming none).
type binds struct {
providerModules map[string]bool
consumers map[string]bool
perHolder bool
forms map[string]bool
}
// The credential forms a plan entry names (issue 768), as the provider's offer declares them.
const (
// FormShared is one value every holder shares (ADR 0158): the offer's credential is one of its own secrets.
FormShared = "shared"
// FormPair is a login of its own for each consumer (ADR 0048, ADR 0158 §11): the offer says no credential.
FormPair = "pair"
// FormIssued is a value the provider's software issues (ADR 0305): the mesh never makes it.
FormIssued = "issued"
// FormGiven is a value only a person gives (ADR 0337): the mesh never makes it.
FormGiven = "given"
)
// credentialForms are every form a plan entry may name.
var credentialForms = []string{FormShared, FormPair, FormIssued, FormGiven}
// form is the one form the plans name for k, or why the vault cannot trust one.
func (b *binds) form(k pairKey) (string, string) {
if b == nil || len(b.forms) == 0 || b.forms[""] {
return "", unknownForm(k)
}
var named []string
for f := range b.forms {
if !slices.Contains(credentialForms, f) {
return "", fmt.Sprintf("a plan names %q for %s from %s, which is not a credential form the vault knows "+
"(%s); it makes nothing", f, k.provision, k.provider, strings.Join(credentialForms, ", "))
}
named = append(named, f)
}
if len(named) > 1 {
sort.Strings(named)
return "", fmt.Sprintf("the plans binding %s from %s disagree on its credential form (%s); it makes nothing",
k.provision, k.provider, strings.Join(named, ", "))
}
return named[0], ""
}
// pairRow is one row the move brought in.
type pairRow struct {
consumer, module string
rec Record
}
// bindings is what one pass read: every plan's bound provisions, which assignments there are, and which of them
// are handed to the provisioner.
type bindings struct {
bound map[pairKey]*binds
assigned map[string]bool
handed map[string]bool
}
// plannedProvisions is as much of an assignment as the pass reads.
type plannedProvisions struct {
Node string `json:"node"`
Module string `json:"module"`
Provisioning *struct {
Provisions []struct {
Name string `json:"name"`
Provision string `json:"provision"`
Local string `json:"local"`
Provider string `json:"provider"`
ProviderModule string `json:"provider-module"`
CredentialForm string `json:"credential-form"`
// Contributes is what the consumer tells its provider (issue 776); the vault does not act on it.
Contributes map[string]any `json:"contributes"`
} `json:"provisions"`
} `json:"provisioning"`
}
// readBindings reads every assignment. An assignment that cannot be read stops the pass: a bind missed would read
// as an unbind, and remake a value its holder still uses.
func readBindings(m Mirror) (bindings, error) {
b := bindings{bound: map[pairKey]*binds{}, assigned: map[string]bool{}, handed: map[string]bool{}}
if m.Keys == nil || m.Read == nil {
return b, errors.New("the vault cannot read the assignments, so it cannot see who binds a provider")
}
keys, err := m.Keys()
if err != nil {
return b, fmt.Errorf("the assignments' keys could not be read: %w", err)
}
for _, key := range keys {
raw, err := m.Read(key)
if err != nil {
return b, fmt.Errorf("the assignment %s could not be read: %w", key, err)
}
if raw == nil {
continue
}
var a plannedProvisions
if err := json.Unmarshal(raw, &a); err != nil || a.Node+"."+a.Module != key {
return b, fmt.Errorf("the assignment %s is not the controller's", key)
}
b.assigned[key] = true
if a.Provisioning == nil {
continue
}
b.handed[key] = true
for _, p := range a.Provisioning.Provisions {
if !namePart.MatchString(p.Provider) || !namePart.MatchString(p.Provision) {
continue
}
k := pairKey{p.Provider, p.Provision}
if b.bound[k] == nil {
b.bound[k] = &binds{providerModules: map[string]bool{}, consumers: map[string]bool{},
forms: map[string]bool{}}
}
b.bound[k].forms[p.CredentialForm] = true
if p.ProviderModule != "" {
b.bound[k].providerModules[p.ProviderModule] = true
}
// A name on an entry of the login-per-consumer form is that login's file (issue 779), not a value issued
// for one holder: the provider still gets its logins as rows.
if p.Name != "" && p.CredentialForm != FormPair {
b.bound[k].perHolder = true
}
b.bound[k].consumers[consumerOf(a.Node, a.Module, p.Local)] = true
}
}
return b, nil
}
func consumerOf(node, module, local string) string {
if local == "" {
return node + "/" + module
}
return node + "/" + module + "/" + local
}
// pairRows is every row the move brought in, by provider and provision.
func (v *Vault) pairRows() (map[pairKey][]pairRow, error) {
all, err := v.Store.List()
if err != nil {
return nil, err
}
out := map[pairKey][]pairRow{}
for _, rec := range all {
parts := strings.Split(rec.Name, "/")
if parts[0] != "pair" || (len(parts) != 5 && len(parts) != 6) {
continue
}
k := pairKey{parts[1], parts[2]}
out[k] = append(out[k], pairRow{consumer: parts[3], module: parts[4], rec: rec})
}
return out, nil
}
// PairOutcome is what one pass did, or did not do, for one provider and provision.
type PairOutcome struct {
Name string `json:"name"`
Outcome string `json:"outcome"`
Generation int `json:"generation,omitempty"`
Why string `json:"why,omitempty"`
}
// What a pass did for a provider and provision.
const (
PairMade = "made"
PairRemade = "remade"
PairAdopted = "adopted"
PairRecorded = "recorded"
PairUnchanged = "unchanged"
PairHeld = "held"
PairPerPair = "waits-for-rotation"
PairPerHolder = "per-holder"
)
// sharedHolders is who holds a provider's value now: its provider module and the consumers bound to it.
func sharedHolders(k pairKey, b *binds) (string, []string) {
var modules, consumers []string
if b != nil {
for m := range b.providerModules {
modules = append(modules, m)
}
for c := range b.consumers {
consumers = append(consumers, c)
}
}
sort.Strings(modules)
sort.Strings(consumers)
return strings.Join(modules, ","), consumers
}
// heldBack says why the vault may not make a value for k now, or "": a holder that would never get it.
func heldBack(k pairKey, b *binds, rows []pairRow, seen bindings) string {
if b == nil || len(b.providerModules) == 0 {
return fmt.Sprintf("no plan names the module serving %s on %s, so the vault cannot tell the provider gets "+
"a value it makes", k.provision, k.provider)
}
for m := range b.providerModules {
if !seen.handed[k.provider+"."+m] {
return fmt.Sprintf("the provider %s on %s is not handed to the provisioner, so it would never get a "+
"value the vault makes (ADR 0349 clause 17)", m, k.provider)
}
}
for _, r := range rows {
key := r.consumer + "." + r.module
if seen.assigned[key] && !seen.handed[key] {
return fmt.Sprintf("%s on %s holds %s from %s and is not handed to the provisioner, so it would never "+
"get a value the vault makes (ADR 0349 clause 17)", r.module, r.consumer, k.provision, k.provider)
}
}
return ""
}
// oneStamp is the generation stamp every row shares, with their one origin and value, when they all share one; or
// why not.
func (v *Vault) oneStamp(rows []pairRow) (stamp, origin, value string, madeAt time.Time, why string) {
for i, r := range rows {
if r.rec.MovedFrom == nil || r.rec.MovedFrom.Generation == "" {
return "", "", "", time.Time{}, fmt.Sprintf("%s carries no generation stamp: a login of its own (ADR 0048)",
r.rec.Name)
}
opened, err := seal.Open(r.rec.AtRest, v.Own.PrivateKey)
if err != nil {
return "", "", "", time.Time{}, fmt.Sprintf("%s does not open with the vault's key: %v", r.rec.Name, err)
}
o := r.rec.Origin
if o == "" {
o = OriginMade
}
if i == 0 {
stamp, origin, value, madeAt = r.rec.MovedFrom.Generation, o, opened, r.rec.MadeAt
continue
}
if r.rec.MovedFrom.Generation != stamp || opened != value || o != origin {
return "", "", "", time.Time{}, fmt.Sprintf("the rows of one provider and provision carry different "+
"generations, values or origins (%s among them): logins of their own (ADR 0048)", r.rec.Name)
}
if r.rec.MadeAt.Before(madeAt) {
madeAt = r.rec.MadeAt
}
}
return stamp, origin, value, madeAt, ""
}
// PairPass looks at every provider and provision the plans bind or the move brought in, and makes, remakes or
// adopts each one's shared value as the rules above say. It returns what it did for each; an error is a pass that
// could not read what it needs, and did nothing.
func (v *Vault) PairPass() ([]PairOutcome, error) {
seen, err := readBindings(v.Assignments)
if err != nil {
return nil, err
}
rows, err := v.pairRows()
if err != nil {
return nil, err
}
keys := map[pairKey]bool{}
for k := range seen.bound {
keys[k] = true
}
for k := range rows {
keys[k] = true
}
all, err := v.Store.List()
if err != nil {
return nil, err
}
for _, rec := range all {
parts := strings.Split(rec.Name, "/")
if len(parts) == 3 && parts[0] == "provision" {
if _, recorded, _ := v.Store.ReadShared(rec.Name); recorded {
keys[pairKey{parts[1], parts[2]}] = true
}
}
}
ordered := make([]pairKey, 0, len(keys))
for k := range keys {
ordered = append(ordered, k)
}
sort.Slice(ordered, func(i, j int) bool { return ordered[i].name() < ordered[j].name() })
var out []PairOutcome
var failed []error
for _, k := range ordered {
o, err := v.pairOne(k, seen.bound[k], rows[k], seen)
if err != nil {
failed = append(failed, fmt.Errorf("%s: %w", k.name(), err))
o = PairOutcome{Name: k.name(), Outcome: PairHeld, Why: err.Error()}
}
out = append(out, o)
}
return out, errors.Join(failed...)
}
// pairOne is one provider and provision's part of a pass.
func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (PairOutcome, error) {
name := k.name()
if b != nil && b.perHolder {
return PairOutcome{Name: name, Outcome: PairPerHolder,
Why: "a plan names a credential per holder for it (issue 757): there is no one shared value"}, nil
}
rec, held, err := v.Store.Read(name)
if err != nil {
return PairOutcome{}, err
}
sh, recorded, err := v.Store.ReadShared(name)
if err != nil {
return PairOutcome{}, err
}
// The provider's module, when no plan names it any more (its last consumer unbound), is the one recorded.
if b == nil {
b = &binds{providerModules: map[string]bool{}, consumers: map[string]bool{}}
}
if len(b.providerModules) == 0 && recorded && sh.ProviderModule != "" {
for _, m := range strings.Split(sh.ProviderModule, ",") {
b.providerModules[m] = true
}
}
providerModule, consumers := sharedHolders(k, b)
record := func(generation int, adopted string) error {
return v.Store.WriteShared(Shared{Name: name, Provider: k.provider, Provision: k.provision,
ProviderModule: providerModule, Consumers: consumers, Generation: generation, At: v.Now().UTC(),
Adopted: adopted})
}
switch {
case !held && len(rows) > 0:
stamp, origin, value, madeAt, why := v.oneStamp(rows)
if why != "" {
// A consumer bound since the move, that has no row yet, gets a login of its own; the rows there are
// never touched (issue 771).
if form, formWhy := b.form(k); formWhy == "" && form == FormPair {
have := map[string]bool{}
for _, r := range rows {
have[strings.Join(strings.Split(r.rec.Name, "/")[3:], "/")] = true
}
var missing []string
for _, c := range consumers {
if !have[c] {
missing = append(missing, c)
}
}
if len(missing) > 0 {
if held := heldBack(k, b, rows, seen); held != "" {
return PairOutcome{Name: name, Outcome: PairHeld, Why: held}, nil
}
return v.firstLogins(k, missing)
}
}
return PairOutcome{Name: name, Outcome: PairPerPair, Why: why + "; each consumer keeps a value of its " +
"own for ever (ADR 0048, ADR 0158 §11), and a rotation remakes each one's own, never one shared value"}, nil
}
// Adopted as it is: the holders the rows name hold it, and a bind since then is a remake on the next pass.
changed, err := v.keepWith(name, value, "the vault", "", origin, keepOptions{neverReplace: true, madeAt: madeAt,
movedFrom: &MovedFrom{By: "the move's pair rows", Generation: stamp}})
value = ""
if err != nil {
return PairOutcome{}, err
}
var fromRows []string
for _, r := range rows {
parts := strings.Split(r.rec.Name, "/")
fromRows = append(fromRows, strings.Join(parts[3:], "/"))
}
consumers = fromRows
if err := record(changed.Generation, stamp); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: name, Outcome: PairAdopted, Generation: changed.Generation}, nil
case !held:
if len(consumers) == 0 {
return PairOutcome{Name: name, Outcome: PairUnchanged}, nil
}
if why := heldBack(k, b, rows, seen); why != "" {
return PairOutcome{Name: name, Outcome: PairHeld, Why: why}, nil
}
form, why := b.form(k)
switch {
case why != "":
return PairOutcome{Name: name, Outcome: PairHeld, Why: why}, nil
case form == FormShared:
changed, err := v.mintShared(name)
if err != nil {
return PairOutcome{}, err
}
if err := record(changed.Generation, ""); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: name, Outcome: PairMade, Generation: changed.Generation}, nil
case form == FormPair:
return v.firstLogins(k, consumers)
case form == FormIssued:
return PairOutcome{Name: name, Outcome: PairHeld, Why: "its credential is issued by the provider's " +
"software (ADR 0305), which the vault never makes; `secret issued` brings it in"}, nil
}
return PairOutcome{Name: name, Outcome: PairHeld, Why: "its credential is given by a person (ADR 0337), " +
"which the vault never makes; `secret accept` brings it in"}, nil
case rec.Origin == OriginAccepted || rec.Origin == OriginIssued:
// Never remade: who holds it is recorded, so each gets it on request.
if recorded && slices.Equal(sh.Consumers, consumers) && sh.ProviderModule == providerModule {
return PairOutcome{Name: name, Outcome: PairUnchanged, Generation: rec.Generation}, nil
}
if err := record(rec.Generation, sh.Adopted); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: name, Outcome: PairRecorded, Generation: rec.Generation,
Why: "a value " + rec.Origin + " is not remade (ADR 0158 §8, ADR 0305 §9); its holders are recorded"}, nil
case !recorded:
// Held but never recorded (made by a rotation before any plan bound it): who binds it now holds it.
if err := record(rec.Generation, ""); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: name, Outcome: PairRecorded, Generation: rec.Generation}, nil
case slices.Equal(sh.Consumers, consumers):
return PairOutcome{Name: name, Outcome: PairUnchanged, Generation: rec.Generation}, nil
}
if why := heldBack(k, b, rows, seen); why != "" {
return PairOutcome{Name: name, Outcome: PairHeld, Generation: rec.Generation, Why: why}, nil
}
changed, err := v.mintShared(name)
if err != nil {
return PairOutcome{}, err
}
if err := record(changed.Generation, ""); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: name, Outcome: PairRemade, Generation: changed.Generation}, nil
}
// mintShared makes the provision's one credential anew, for all its holders in one act: a new generation and
// `secret-changed`, which tells every holder to ask again.
func (v *Vault) mintShared(name string) (Changed, error) {
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Changed{}, err
}
return v.keep(name, base64.RawURLEncoding.EncodeToString(raw), "the vault", "", OriginMade)
}
// firstLogins makes each bound consumer of a per-login provider the vault holds no row of its first login of its
// own (ADR 0048, ADR 0158 §11, issue 768): `pair/<provider>/<provision>/<consumer>/<module>[/<local>]`, a value
// apart for each, never one shared value. Once made they are rows, and every later pass keeps them per pair.
func (v *Vault) firstLogins(k pairKey, consumers []string) (PairOutcome, error) {
var failed []error
for _, c := range consumers {
parts := strings.SplitN(c, "/", 3)
local := ""
if len(parts) == 3 {
local = parts[2]
}
row, err := PairSecret(k.provider, k.provision, parts[0], parts[1], local)
if err != nil {
failed = append(failed, err)
continue
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return PairOutcome{}, err
}
if _, err := v.keepWith(row, base64.RawURLEncoding.EncodeToString(raw), "the vault", "", OriginMade,
keepOptions{neverReplace: true}); err != nil {
failed = append(failed, fmt.Errorf("%s: %w", row, err))
}
}
if err := errors.Join(failed...); err != nil {
return PairOutcome{}, err
}
return PairOutcome{Name: k.name(), Outcome: PairMade, Why: fmt.Sprintf("a first login of its own for each of "+
"%d consumers (ADR 0048, ADR 0158 §11)", len(consumers))}, nil
}
// unknownForm is why the vault makes nothing for a provider and provision it has no row of while no plan names its
// offer's credential form: whether the offer shares one credential (ADR 0158) or keeps a login per consumer (ADR
// 0048) is the offer's, and a plan from a controller before issue 768 does not say which.
func unknownForm(k pairKey) string {
return fmt.Sprintf("the vault holds no row of %s from %s, and no plan says whether its offer shares one credential "+
"(ADR 0158) or keeps a login per consumer (ADR 0048, §11); it makes nothing until it knows", k.provision,
k.provider)
}
// rotatePerPair is the rotation of a provider whose consumers each keep a login of their own (ADR 0048, ADR 0158
// §11): every consumer's own value is remade apart, each a value of its own, and no shared value is made. A value
// accepted or issued is left as it is. Refused whole while any holder would never get its new value. Narrowed to one
// consumer, only that consumer's own logins are remade (issue 767), and a consumer that holds none is refused.
func (v *Vault) rotatePerPair(name string, one Narrow, by string) (Changed, error) {
parts := strings.Split(name, "/")
k := pairKey{parts[1], parts[2]}
rows, err := v.pairRows()
if err != nil {
return Changed{}, err
}
if len(rows[k]) == 0 {
return Changed{}, fmt.Errorf("not rotated: the vault holds no secret named %s, and %s", name, unknownForm(k))
}
if _, _, _, _, why := v.oneStamp(rows[k]); why == "" {
return Changed{}, fmt.Errorf("not rotated: the rows of %s share one value not adopted yet; the next pass "+
"adopts it, then rotate it", name)
}
if why := v.rotationHeldBack(name); why != "" {
return Changed{}, fmt.Errorf("not rotated: %s", why)
}
if !slices.ContainsFunc(rows[k], one.holds) {
return Changed{}, fmt.Errorf("not rotated: %s holds no login of %s from %s", one, k.provision, k.provider)
}
out := Changed{Name: name, At: v.Now().UTC(), By: by}
var failed []error
for _, r := range rows[k] {
if !one.holds(r) {
continue
}
if r.rec.Origin == OriginAccepted || r.rec.Origin == OriginIssued {
continue
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return out, err
}
changed, err := v.keep(r.rec.Name, base64.RawURLEncoding.EncodeToString(raw), by, ControllerCaller, OriginMade)
if err != nil {
failed = append(failed, fmt.Errorf("%s: %w", r.rec.Name, err))
continue
}
out.Each = append(out.Each, changed)
}
return out, errors.Join(failed...)
}
// rotationHeldBack says why a rotation of a provision's one credential would leave a holder behind, or "".
func (v *Vault) rotationHeldBack(name string) string {
parts := strings.Split(name, "/")
if len(parts) != 3 || parts[0] != "provision" || v.Assignments.Keys == nil {
return ""
}
k := pairKey{parts[1], parts[2]}
seen, err := readBindings(v.Assignments)
if err != nil {
return err.Error()
}
rows, err := v.pairRows()
if err != nil {
return err.Error()
}
return heldBack(k, seen.bound[k], rows[k], seen)
}
// sharedRefusal says why caller (`provisioner.<node>`) may not have the provision's one credential name, or "": it
// is handed to the machines the vault made it for, the provider's and each bound consumer's, while the provider's
// assignment there is handed to the provisioner, or a consumer's plan there still names it from that provider.
func sharedRefusal(store *Store, read AssignmentReader, node, provider, provision string) string {
name := "provision/" + provider + "/" + provision
if store == nil {
return "the vault keeps no record of who holds " + name
}
sh, ok, err := store.ReadShared(name)
if err != nil {
return err.Error()
}
if !ok {
return fmt.Sprintf("the vault made %s for nobody yet: it is handed to the machines its provider and its "+
"consumers run on once their plans bind it (ADR 0349 clause 8)", name)
}
if !slices.Contains(sh.holderNodes(), node) {
return fmt.Sprintf("%s is held by %s, not by a module on %s", name, strings.Join(sh.holderNodes(), ", "), node)
}
if read == nil {
return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name"
}
if node == provider && sh.ProviderModule != "" {
for _, m := range strings.Split(sh.ProviderModule, ",") {
if raw, err := read(node + "." + m); err == nil && raw != nil {
var a plannedProvisions
if json.Unmarshal(raw, &a) == nil && a.Node == node && a.Module == m && a.Provisioning != nil {
return ""
}
}
}
}
for _, c := range sh.Consumers {
parts := strings.SplitN(c, "/", 3)
if parts[0] != node {
continue
}
raw, err := read(node + "." + parts[1])
if err != nil || raw == nil {
continue
}
var a plannedProvisions
if json.Unmarshal(raw, &a) != nil || a.Node != node || a.Module != parts[1] || a.Provisioning == nil {
continue
}
for _, p := range a.Provisioning.Provisions {
if p.Name == "" && p.Provision == provision && p.Provider == provider {
return ""
}
}
}
return fmt.Sprintf("no module on %s that the vault made %s for is handed to the provisioner and bound to it now",
node, name)
}
// pairPass is one minute's PairPass, saying each outcome when it differs from the last pass's, so a provider held
// back is said once and again when it changes, not every minute.
func (e *Env) pairPass() {
if e.Vault == nil {
return
}
outcomes, err := e.Vault.PairPass()
e.mu.Lock()
defer e.mu.Unlock()
if err != nil && err.Error() != e.lastPairFailure {
say("the pass over the providers' shared credentials did not finish: %v", err)
}
e.lastPairFailure = ""
if err != nil {
e.lastPairFailure = err.Error()
}
if e.pairsSaid == nil {
e.pairsSaid = map[string]string{}
}
for _, o := range outcomes {
said := o.Outcome + ": " + o.Why
if o.Outcome == PairUnchanged || e.pairsSaid[o.Name] == said {
continue
}
e.pairsSaid[o.Name] = said
if o.Why == "" {
say("%s %s (generation %d)", o.Name, o.Outcome, o.Generation)
} else {
say("%s %s: %s", o.Name, o.Outcome, o.Why)
}
}
}
// pairRowRefusal says why caller's machine node may not have the pair row name (`pair/<provider>/<provision>/
// <consumer>/<module>[/<local>]`, a consumer's own login, ADR 0048), or "": it is handed to that consumer's machine
// while the consumer's plan there names the provision from that provider under that local name, and to the
// provider's machine while the provider module that plan names is handed to the provisioner there. Another
// consumer, on the same machine or not, is refused: each gets only its own.
func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
provider, provision, consumer, module, local := parts[1], parts[2], parts[3], parts[4], ""
if len(parts) == 6 {
local = parts[5]
}
if node != consumer && node != provider {
return fmt.Sprintf("%s is the login of %s on %s from %s, and %s is neither", strings.Join(parts, "/"),
module, consumer, provider, node)
}
if read == nil {
return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name"
}
raw, err := read(consumer + "." + module)
if err != nil || raw == nil {
return fmt.Sprintf("%s is not on %s", module, consumer)
}
var a plannedProvisions
if json.Unmarshal(raw, &a) != nil || a.Node != consumer || a.Module != module {
return fmt.Sprintf("the assignment %s.%s is not the controller's", consumer, module)
}
if a.Provisioning == nil {
return fmt.Sprintf("%s on %s is not handed to the provisioner, so its login reaches it in its declaration "+
"(ADR 0349 clause 17)", module, consumer)
}
for _, p := range a.Provisioning.Provisions {
// An unnamed entry, or a named one of the same form (the consumer's login file, issue 779): one rule.
if (p.Name != "" && p.CredentialForm != FormPair) || p.Provision != provision ||
p.Provider != provider || p.Local != local {
continue
}
if node == consumer {
return ""
}
if p.ProviderModule != "" {
if praw, err := read(provider + "." + p.ProviderModule); err == nil && praw != nil {
var pa plannedProvisions
if json.Unmarshal(praw, &pa) == nil && pa.Node == provider && pa.Provisioning != nil {
return ""
}
}
}
return fmt.Sprintf("the provider of %s on %s is not handed to the provisioner", provision, provider)
}
return fmt.Sprintf("%s's plan on %s does not name %s from %s under that name", module, consumer, provision, provider)
}