The three tests of the controller's own module.json (process, claimed seat, addresses), the Dockerfile and Makefile pin test, the build-agent registry test and the manifest-in-use seat test move with their old package, as tests of mesh-sdk's exported API; a moved test is exempt from failing first.
253 lines
10 KiB
Go
253 lines
10 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
)
|
|
|
|
// A provision whose credential only a person outside the mesh can give (novox/hq ADR 0337, issue 164): a Servarr
|
|
// app's API key, an indexer's key, a downloader's password. The provider's software holds the value; the mesh can
|
|
// neither make one that would work nor fetch it. So the mesh never makes one (§2), a person gives it once per
|
|
// provider and it is sealed to every consumer of that provider (§3), and until then a consumer holds no credential
|
|
// for it and is told so (§4), and the rest of it runs.
|
|
|
|
// NotGivenError is a pair credential only a person can give, which no person gave for this pair under the keys its
|
|
// two ends have now: the consumer receives no credential for it, and waits (ADR 0337 §4, §8). It is not a fault of
|
|
// the machine, so the plan does not refuse the machine for it.
|
|
type NotGivenError struct {
|
|
Provision, Consumer, ConsumerModule, Provider, ProviderModule, Local string
|
|
// Own is set when the credential is the provider's own secret issued outside (ADR 0158, ADR 0228), and names it.
|
|
Own string
|
|
// Why is why no value is held, in words that never carry one.
|
|
Why string
|
|
}
|
|
|
|
func (e *NotGivenError) Error() string {
|
|
if e.Own != "" {
|
|
return fmt.Sprintf("%s on %s holds no %s from %s: %s; give it with `secret accept %s %s %s`",
|
|
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, e.Provider, e.ProviderModule, e.Own)
|
|
}
|
|
return fmt.Sprintf("%s on %s holds no %s from %s: %s; it is issued outside the mesh and given with `%s`",
|
|
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, acceptLine(e.Provider, e.ProviderModule, e.Provision))
|
|
}
|
|
|
|
// IsNotGiven is whether an error is a credential only a person gives, not given yet.
|
|
func IsNotGiven(err error) bool {
|
|
var ng *NotGivenError
|
|
return errors.As(err, &ng)
|
|
}
|
|
|
|
// acceptLine is the one line that gives a provision's credential issued outside, for every consumer of a provider.
|
|
func acceptLine(provider, providerModule, provision string) string {
|
|
if providerModule == "" {
|
|
providerModule = "<provider module>"
|
|
}
|
|
return fmt.Sprintf("secret accept %s %s %s", provider, providerModule, provision)
|
|
}
|
|
|
|
// AcceptLine is acceptLine, for the controller's words.
|
|
func AcceptLine(provider, providerModule, provision string) string {
|
|
return acceptLine(provider, providerModule, provision)
|
|
}
|
|
|
|
// issuedOutside is whether a module in the catalogue offers the provision with a credential issued outside the
|
|
// mesh, and which.
|
|
func (i *Inventory) issuedOutside(ctx context.Context, provision string) (string, bool, error) {
|
|
shelf, err := i.Catalogue(ctx)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
module, outside := catalogue.OffersIssuedOutside(shelf, provision)
|
|
return module, outside, nil
|
|
}
|
|
|
|
// ownIssuedOutside is whether a provider's own secret says only an outside party issues it (ADR 0228).
|
|
func (i *Inventory) ownIssuedOutside(ctx context.Context, providerModule, own string) (string, bool, error) {
|
|
if providerModule == "" {
|
|
return own, false, nil
|
|
}
|
|
m, err := i.declared(ctx, providerModule)
|
|
if err != nil {
|
|
return own, false, err
|
|
}
|
|
return own, m.OwnSecrets[own].IssuedBy == catalogue.IssuedOutside, nil
|
|
}
|
|
|
|
// GivenPairSecret is the credential of a pair whose provision is issued outside the mesh, as a person gave it, and
|
|
// never one the mesh makes (ADR 0337 §2, §4). It writes nothing. A value is answered only when a person gave it
|
|
// (`accepted`) and it is sealed to the keys both ends have now; otherwise a NotGivenError says why — never given, or
|
|
// given under a key one end no longer has (§8), or a value the mesh made before the rule, which is not given and
|
|
// never delivered again (§2).
|
|
func (i *Inventory) GivenPairSecret(ctx context.Context, name, consumer, consumerModule, provider, providerModule,
|
|
local string) (Secret, error) {
|
|
consumerKey, err := i.SealingKeyOf(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerKey, err := i.SealingKeyOf(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
consumerNode, err := i.NodeByName(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
notGiven := func(why string) (Secret, error) {
|
|
return Secret{}, &NotGivenError{Provision: name, Consumer: consumer, ConsumerModule: consumerModule,
|
|
Provider: provider, ProviderModule: providerModule, Local: local, Why: why}
|
|
}
|
|
var held Secret
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
|
|
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
|
|
name, consumerNode.ID, consumerModule, providerNode.ID, local).
|
|
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
|
|
switch {
|
|
case errors.Is(err, pgx.ErrNoRows):
|
|
return notGiven("nobody gave it")
|
|
case err != nil:
|
|
return Secret{}, err
|
|
case held.Origin != OriginAccepted:
|
|
return notGiven("the value held was made by the mesh, which " + providerModule + "'s software would refuse")
|
|
case consumerKey == "" || providerKey == "" || held.ConsumerKey != consumerKey || held.ProviderKey != providerKey:
|
|
return notGiven("it was given under a sealing key one end no longer has")
|
|
}
|
|
held.Name, held.Consumer, held.Provider = name, consumer, provider
|
|
held.ConsumerModule, held.Local = consumerModule, local
|
|
return held, nil
|
|
}
|
|
|
|
// PairEnd is one consumer of a provision from one provider: the machine, the module there and its local name.
|
|
type PairEnd struct {
|
|
Consumer, ConsumerModule, Local string
|
|
}
|
|
|
|
// ProvisionAccepted is what one accept for a provider sealed (ADR 0337 §3): the consumers it reached, those it could
|
|
// not (no sealing key on their machine), and the machines to send.
|
|
type ProvisionAccepted struct {
|
|
Sealed, Unsealable []PairEnd
|
|
Machines []string
|
|
}
|
|
|
|
// AcceptSecretForProvision seals one value a person gave for a provision issued outside the mesh to the provider's
|
|
// machine, to every consumer named, and to the operator, in one act under one generation, each row `accepted`
|
|
// (ADR 0337 §3, issue 165); the plaintext is not kept. The consumers are every one that binds the provision from
|
|
// this provider now, which the caller finds by resolving every machine. A consumer whose machine has no sealing key
|
|
// is not sealed to, and said.
|
|
func (i *Inventory) AcceptSecretForProvision(ctx context.Context, provision, provider, providerModule, value string,
|
|
consumers []PairEnd) (ProvisionAccepted, error) {
|
|
m, err := i.declared(ctx, providerModule)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
if !m.IssuedOutsideOf(provision) {
|
|
return ProvisionAccepted{}, fmt.Errorf("%s does not offer %q with a credential issued outside the mesh "+
|
|
"(\"credential\": {\"issued-by\": \"outside\"}, ADR 0337), so its consumers' credentials are not given "+
|
|
"once for the provider", providerModule, provision)
|
|
}
|
|
if value == "" {
|
|
return ProvisionAccepted{}, errors.New("there is nothing to seal")
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
providerKey, err := i.SealingKeyOf(ctx, provider)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
if providerKey == "" {
|
|
return ProvisionAccepted{}, fmt.Errorf("%s has no sealing key, so nothing can be sealed to it; a node joins to get one", provider)
|
|
}
|
|
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
generation := secrets.Stamp()
|
|
var out ProvisionAccepted
|
|
machines := map[string]bool{provider: true}
|
|
tx, err := i.store.Pool().Begin(ctx)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
for _, c := range consumers {
|
|
consumerNode, err := i.NodeByName(ctx, c.Consumer)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
consumerKey, err := i.SealingKeyOf(ctx, c.Consumer)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
if consumerKey == "" {
|
|
out.Unsealable = append(out.Unsealable, c)
|
|
continue
|
|
}
|
|
sealed, err := secrets.Accept(value, consumerKey, providerKey)
|
|
if err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
if _, err := tx.Exec(ctx,
|
|
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
|
|
consumer_key, provider_key, operator_sealed, operator_key, origin, local, generation)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
on conflict (name, local, consumer, consumer_module, provider) do update set
|
|
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
|
|
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
|
|
created_at = now(), origin = excluded.origin, generation = excluded.generation,
|
|
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
|
provision, consumerNode.ID, c.ConsumerModule, providerNode.ID,
|
|
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
|
|
forOperator, operatorKey, OriginAccepted, c.Local, generation); err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
out.Sealed = append(out.Sealed, c)
|
|
machines[c.Consumer] = true
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return ProvisionAccepted{}, err
|
|
}
|
|
for n := range machines {
|
|
out.Machines = append(out.Machines, n)
|
|
}
|
|
sort.Strings(out.Machines)
|
|
return out, nil
|
|
}
|
|
|
|
// GivenPairsOf is every provision a module on a machine holds a value a person gave for, sealed to the keys both
|
|
// ends have now: what a wait for a provision's credential is checked against (ADR 0337 §6).
|
|
func (i *Inventory) GivenPairsOf(ctx context.Context, machine, module string) (map[string]bool, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select s.name
|
|
from secret s
|
|
join node c on c.id = s.consumer
|
|
join node p on p.id = s.provider
|
|
where c.name = $1 and s.consumer_module = $2 and s.origin = 'accepted'
|
|
and s.consumer_key = c.sealing_key and s.provider_key = p.sealing_key`, machine, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]bool{}
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return nil, err
|
|
}
|
|
out[name] = true
|
|
}
|
|
return out, rows.Err()
|
|
}
|