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 = "" } 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() }