mesh/merge-gate head c9dd07f919a2 merged into trunk 404b4409e70e — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head c9dd07f919a2 merged into trunk 404b4409e70e — pass: its merge-check.sh passed (run 2)
mesh/delivery delivered
A Servarr app's API key, an indexer's key and a downloader's password are values the provider's
software holds; the pair path made a random value for every consumer of them, sealed it to both ends
and reported success, and the provider refused it. An offer may now say `"credential": {"issued-by":
"outside"}`: SecretFor never makes a value for it, the plan reads such a pair as given or not given
and sends the consumer no credential file until a person gives it, without refusing its machine;
remakeShared and SharedSecretFor never make a provider's own secret issued outside either.
`secret accept <provider's node> <provider module> <provision>` seals one value to every consumer that
binds the provision from that provider, the provider and the operator, under one generation; the
per-pair form is refused for such a provision, and rotate names the accept. A consumer may wait for a
provision's credential: the controller excuses the wait only when the store bears it out, and its
needs-operator condition names the one accept line.
Adds the replay of issue 164 (TestReplay164), written with only what the controller had before.
253 lines
10 KiB
Go
253 lines
10 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-controller/internal/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()
|
|
}
|