815 lines
32 KiB
Go
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)
|
|
}
|