198 lines
8.8 KiB
Go
198 lines
8.8 KiB
Go
//go:build integration
|
|
|
|
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)
|
|
}
|
|
}
|