175 lines
6.3 KiB
Go
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
|
|
}
|