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

175 lines
6.3 KiB
Go

package main
// The vault's outcome for each credential it provides (novox/hq ADR 0570 clauses 3, 6, 7): an entry in its own state
// `bound`, one per credential provision `<node>.<consumer>.secret-<provision>[.<local>]`, which the holder of
// mesh-provisioner reads to set that key's state. The entry names the value by the vault's own name for it
// (meshprovisionerclient.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
// provided; one only the provider's software issues, or only a person gives, waits for them; one the vault has not
// made yet has no entry, and the key stays asked. An offer whose form is none has no credential and no entry. An entry
// is let go only once its key is retiring with the operator's warrant, or gone.
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
)
// BoundState is the vault's state of outcome entries.
const BoundState = "bound"
// Bound is the vault's own state `bound`, and what it reads of the seat's meshprovisionerclient.
type Bound struct {
Get func(key string) ([]byte, error)
Put func(key string, value []byte) error
Delete func(key string) error
Keys func() ([]string, error)
// Provision is one key of mesh-provisioner's provisions, nil for none.
Provision func(key string) ([]byte, error)
}
// BoundOutcome is one entry of `bound`, in the provider helper's entry layout with the generation beside it.
type BoundOutcome struct {
Provider string `json:"provider"`
ProviderModule string `json:"provider-module"`
Provision string `json:"provision"`
Consumer string `json:"consumer"`
Module string `json:"module"`
Local string `json:"local,omitempty"`
VaultName string `json:"vault-name,omitempty"`
State string `json:"state"`
Why string `json:"why,omitempty"`
Generation int `json:"generation,omitempty"`
}
// OutcomePass writes each credential's outcome that differs and lets go each entry no plan names whose key is retiring
// with the operator's warrant or gone. It answers what it wrote and removed.
func (v *Vault) OutcomePass() ([]string, error) {
if v.Bound.Get == nil || v.Assignments.Keys == nil {
return nil, nil
}
want, err := v.outcomes()
if err != nil {
return nil, err
}
var said []string
for _, key := range sortedOutcomeKeys(want) {
raw, _ := json.Marshal(want[key])
if have, err := v.Bound.Get(key); err == nil && bytes.Equal(have, raw) {
continue
}
if err := v.Bound.Put(key, raw); err != nil {
return said, fmt.Errorf("the outcome %s: %w", key, err)
}
said = append(said, key+" "+want[key].State)
}
keys, err := v.Bound.Keys()
if err != nil {
return said, err
}
for _, key := range keys {
if _, still := want[key]; still {
continue
}
if !v.letGo(key) {
continue
}
if err := v.Bound.Delete(key); err != nil {
return said, err
}
said = append(said, key+" let go")
}
return said, nil
}
// letGo says an entry no plan names may go: its key is gone, or retiring with the operator's Approve.
func (v *Vault) letGo(key string) bool {
if v.Bound.Provision == nil {
return false
}
raw, err := v.Bound.Provision(key)
if err != nil {
return false
}
if raw == nil {
return true
}
p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == meshprovisionerclient.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
}
// outcomes is every credential's outcome the plans call for, by its key.
func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
keys, err := v.Assignments.Keys()
if err != nil {
return nil, fmt.Errorf("the plans' keys: %w", err)
}
out := map[string]BoundOutcome{}
for _, k := range keys {
raw, err := v.Assignments.Read(k)
if err != nil {
return nil, fmt.Errorf("the plan %s: %w", k, err)
}
if raw == nil {
continue
}
var a plannedProvisions
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
continue
}
consumer := meshprovisionerclient.End{Module: a.Module, Node: a.Node}
for _, p := range a.Provisioning.Provisions {
provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == vaultModule && p.Provision == "secret" {
// The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
// soon as it runs.
key, err := meshprovisionerclient.Key(a.Node, a.Module, p.Provision, p.Local)
if err == nil {
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: vaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: meshprovisionerclient.Provided,
Why: "the vault hands it to the consumer's client on request"}
}
continue
}
ck, ok := meshprovisionerclient.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
if !ok {
continue // form none: no credential, nothing minted
}
parent := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := meshprovisionerclient.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil || name == "" {
continue
}
o := BoundOutcome{Provider: p.Provider, ProviderModule: vaultModule, Provision: cp, Consumer: cn,
Module: cm, Local: cl, VaultName: name}
rec, held, err := v.Store.Read(name)
switch {
case err != nil:
o.State, o.Why = meshprovisionerclient.Failed, "the vault cannot read "+name+": "+err.Error()
case held:
o.State, o.Generation = meshprovisionerclient.Provided, rec.Generation
case p.CredentialForm == FormIssued:
o.State, o.Why = meshprovisionerclient.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
case p.CredentialForm == FormGiven:
o.State, o.Why = meshprovisionerclient.Waits, "a person gives it (ADR 0337): `accept` brings it in"
default:
continue // not made yet: the key stays asked until the pair pass makes it
}
out[ck] = o
}
}
return out, nil
}
func sortedOutcomeKeys(m map[string]BoundOutcome) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}