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.
196 lines
8.7 KiB
Go
196 lines
8.7 KiB
Go
package inventory
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// outsideProvision registers a provider offering tv-api with a credential issued outside the mesh (novox/hq ADR
|
|
// 0337 §1) and two consumers requiring it, on twoNodesWithKeys' machines.
|
|
func outsideProvision(t *testing.T, inv *Inventory) {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "tv", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
|
|
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []string{"requests", "indexer"} {
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1", Requires: []string{"tv-api"},
|
|
Secrets: map[string]string{"tv-api": "/run/tv-api.secret"}}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// pairRows is how many pair rows the store holds for tv-api.
|
|
func pairRows(t *testing.T, inv *Inventory) int {
|
|
t.Helper()
|
|
var n int
|
|
if err := inv.store.Pool().QueryRow(t.Context(), `select count(*) from secret where name = 'tv-api'`).Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
// A credential only a person can give is never made (ADR 0337 §2, issue 164): not for a pair with no row, which is
|
|
// what a newly bound consumer has, and not after a key change; a value the mesh made before the rule is not given.
|
|
func TestACredentialIssuedOutsideIsNeverMade(t *testing.T) {
|
|
inv, ctx := twoNodesWithKeys(t)
|
|
outsideProvision(t, inv)
|
|
|
|
_, err := inv.SecretFor(ctx, "tv-api", "consumer", "requests", "provider", "")
|
|
if !IsNotGiven(err) {
|
|
t.Fatalf("SecretFor made or refused a credential only a person gives, rather than saying it was not given: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
|
|
t.Fatalf("not given does not name the one line that gives it: %v", err)
|
|
}
|
|
if n := pairRows(t, inv); n != 0 {
|
|
t.Fatalf("the store holds %d rows for tv-api, which nobody gave", n)
|
|
}
|
|
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) {
|
|
t.Fatalf("a pair nobody gave read as given: %v", err)
|
|
}
|
|
if n := pairRows(t, inv); n != 0 {
|
|
t.Fatalf("reading the pair wrote %d rows", n)
|
|
}
|
|
|
|
// A value the mesh made before the rule (the minted value of issue 164) is not given.
|
|
if _, err := inv.store.Pool().Exec(ctx, `insert into secret (name, consumer, consumer_module, provider,
|
|
for_consumer, for_provider, consumer_key, provider_key, local)
|
|
select 'tv-api', c.id, 'indexer', p.id, 'x', 'y', c.sealing_key, p.sealing_key, ''
|
|
from node c, node p where c.name = 'consumer' and p.name = 'provider'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "indexer", "provider", "tv", ""); !IsNotGiven(err) ||
|
|
!strings.Contains(err.Error(), "made by the mesh") {
|
|
t.Fatalf("a value the mesh made before the rule read as given: %v", err)
|
|
}
|
|
|
|
// Given, then a key changes: not given, and nothing is made in its place (ADR 0337 §8).
|
|
if _, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key",
|
|
[]PairEnd{{Consumer: "consumer", ConsumerModule: "requests"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); err != nil {
|
|
t.Fatalf("the value given does not read as given: %v", err)
|
|
}
|
|
cn, _ := inv.NodeByName(ctx, "consumer")
|
|
key, _ := aSealingKey(t)
|
|
if err := inv.RecordSealingKey(ctx, cn.ID, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) ||
|
|
!strings.Contains(err.Error(), "sealing key") {
|
|
t.Fatalf("a value given under a key the consumer no longer has read as given: %v", err)
|
|
}
|
|
var origin string
|
|
if err := inv.store.Pool().QueryRow(ctx, `select origin from secret where name = 'tv-api' and consumer_module = 'requests'`).
|
|
Scan(&origin); err != nil || origin != OriginAccepted {
|
|
t.Fatalf("the given row was replaced (origin %q, %v)", origin, err)
|
|
}
|
|
|
|
// Rotate refuses it, naming the accept (ADR 0337 §9).
|
|
err = inv.RotateSecret(ctx, "tv-api", "consumer", "requests", "provider", "")
|
|
if err == nil || !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
|
|
t.Fatalf("rotate of a credential issued outside was not refused naming the accept: %v", err)
|
|
}
|
|
}
|
|
|
|
// One accept for a provider is sealed to every consumer named, to the provider and to the operator, under one
|
|
// generation, each row accepted (ADR 0337 §3, issue 165); a consumer with no sealing key is said, not sealed to.
|
|
func TestOneAcceptForAProviderIsSealedToEveryConsumer(t *testing.T) {
|
|
inv, ctx := twoNodesWithKeys(t)
|
|
outsideProvision(t, inv)
|
|
operatorKey, operatorOpens := aSealingKey(t)
|
|
if _, err := inv.SetOperatorKey(ctx, operatorKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
consumerKey, consumerOpens := aSealingKey(t)
|
|
cn, _ := inv.NodeByName(ctx, "consumer")
|
|
if err := inv.RecordSealingKey(ctx, cn.ID, consumerKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.AddNode(ctx, "keyless"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
accepted, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key", []PairEnd{
|
|
{Consumer: "consumer", ConsumerModule: "requests"}, {Consumer: "consumer", ConsumerModule: "indexer"},
|
|
{Consumer: "keyless", ConsumerModule: "requests"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(accepted.Sealed) != 2 || len(accepted.Unsealable) != 1 || accepted.Unsealable[0].Consumer != "keyless" {
|
|
t.Fatalf("the accept did not seal to both consumers with keys and say the keyless one: %+v", accepted)
|
|
}
|
|
if strings.Join(accepted.Machines, ",") != "consumer,provider" {
|
|
t.Fatalf("the machines to send are the provider's and every consumer's sealed to: %v", accepted.Machines)
|
|
}
|
|
rows, err := inv.store.Pool().Query(ctx, `select consumer_module, origin, coalesce(generation, ''), for_consumer,
|
|
coalesce(operator_sealed, '') from secret where name = 'tv-api'`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
generations := map[string]bool{}
|
|
seen := 0
|
|
for rows.Next() {
|
|
var module, origin, generation, forConsumer, forOperator string
|
|
if err := rows.Scan(&module, &origin, &generation, &forConsumer, &forOperator); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen++
|
|
if origin != OriginAccepted || generation == "" {
|
|
t.Fatalf("%s's row is not accepted under a generation (origin %q)", module, origin)
|
|
}
|
|
generations[generation] = true
|
|
if opened, ok := consumerOpens(forConsumer); !ok || string(opened) != "the app's key" {
|
|
t.Fatalf("%s's copy does not open to the value given", module)
|
|
}
|
|
if opened, ok := operatorOpens(forOperator); !ok || string(opened) != "the app's key" {
|
|
t.Fatalf("%s's operator copy does not open to the value given", module)
|
|
}
|
|
}
|
|
if seen != 2 || len(generations) != 1 {
|
|
t.Fatalf("%d rows under %d generations; want 2 under one", seen, len(generations))
|
|
}
|
|
given, err := inv.GivenPairsOf(ctx, "consumer", "indexer")
|
|
if err != nil || !given["tv-api"] {
|
|
t.Fatalf("the provision given does not read as given for indexer (%v)", err)
|
|
}
|
|
|
|
// Only an offer that says so is given this way.
|
|
if _, err := inv.AcceptSecretForProvision(ctx, "secret", "provider", "gitea", "x", nil); err == nil {
|
|
t.Fatal("a provision not issued outside was given once for its provider")
|
|
}
|
|
}
|
|
|
|
// The provider's own secret issued outside the mesh, named as a provision's shared credential (ADR 0158, ADR 0228),
|
|
// is never made for a consumer that binds: the consumer is told it was not given (ADR 0337 §2, §4).
|
|
func TestASharedOwnSecretIssuedOutsideIsNeverMade(t *testing.T) {
|
|
inv, ctx := twoNodesWithKeys(t)
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "downloader", Version: "1",
|
|
OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password", Taken: catalogue.TakenAtStart,
|
|
IssuedBy: catalogue.IssuedOutside}},
|
|
Provides: []catalogue.Offer{{Name: "downloader-api", Scope: catalogue.ScopeMesh,
|
|
Credential: &catalogue.OfferCredential{Own: "password"}}}}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := inv.SharedSecretFor(ctx, "downloader-api", "consumer", "gitea", "provider", "downloader", "", "password")
|
|
if !IsNotGiven(err) {
|
|
t.Fatalf("a shared own secret issued outside was made or refused rather than not given: %v", err)
|
|
}
|
|
var n int
|
|
if err := inv.store.Pool().QueryRow(ctx, `select count(*) from module_secret where module = 'downloader'`).Scan(&n); err != nil || n != 0 {
|
|
t.Fatalf("the mesh made %d values of an own secret issued outside (%v)", n, err)
|
|
}
|
|
if err := inv.remakeShared(ctx, nil, "", "downloader", "password", "", nil, "", "", ""); err == nil ||
|
|
!strings.Contains(err.Error(), "issued outside") {
|
|
t.Fatalf("remakeShared made a value of an own secret issued outside: %v", err)
|
|
}
|
|
}
|