Read the credentials and the builds into mesh-sdk's composition input, and make nothing (issue 1383)
How each pair credential a node takes stands (held with its sealed consumer copy, the next send makes it, only a person's to give and nobody gave, or refused in the controller's words), the credentials made for each provider's consumers, the own secrets with their origin and key and value, the vault's kept boxes, the bus's users, minted users and memberships, the mesh's authority and each machine's serving key and kept certificate, the repositories built, the current builds and the build of each commit a machine that reads them is sent. A credential no row holds is not made (planFor made it in every mode): the adapter returns it as would-make for perform (issue 1385). New reads in the inventory (OwnSecretRows, PairCredentialState, BusMinted) and the identity (KnownAuthority, KeptCertificate) write nothing.
This commit is contained in:
@@ -53,21 +53,19 @@ func readComposeInput(ctx context.Context, open *stores) (*meshInput, error) {
|
||||
if err := readSettings(ctx, open, &in, plans); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := readCredentialsAndBuilds(ctx, open, &in, plans, shape); err != nil {
|
||||
wouldMake, err := readCredentialsAndBuilds(ctx, open, &in, plans, shape)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
composeInputBuilt()
|
||||
return &meshInput{Input: in}, nil
|
||||
return &meshInput{Input: in, wouldMake: wouldMake}, nil
|
||||
}
|
||||
|
||||
// readCredentialsAndBuilds is the credential and build half of the Input, which issue 1383 fills.
|
||||
func readCredentialsAndBuilds(context.Context, *stores, *compose.Input, map[string]compose.Resolved, inputShape) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// meshInput is the Input an operation composes over.
|
||||
// meshInput is the Input an operation composes over, and the pair credentials its reading found none held for: the ones the
|
||||
// next send makes, which perform makes.
|
||||
type meshInput struct {
|
||||
compose.Input
|
||||
wouldMake []inventory.PairCredential
|
||||
}
|
||||
|
||||
// nodeOf is the node of that name in an Input.
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// The second half of the adapter (issue 1383): the credential and build fields of the Input, read and never made.
|
||||
|
||||
// readCredentialsAndBuilds fills what the first half left: the credentials, the own secrets, the vault's boxes, the bus,
|
||||
// each node's serving key and certificate, and the builds.
|
||||
func readCredentialsAndBuilds(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved,
|
||||
shape inputShape) ([]inventory.PairCredential, error) {
|
||||
if err := readIdentity(ctx, open, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wouldMake, err := readCredentials(ctx, open, in, plans)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wouldMake, readBuilds(ctx, open, in, plans, shape)
|
||||
}
|
||||
|
||||
// readIdentity is the mesh's authority and each node's serving key and the certificate kept for it, for a mesh in which a
|
||||
// module asks for a certificate. It establishes and issues nothing: a certificate not kept yet is composed with a stand-in, and
|
||||
// issued by the send (certificateFor).
|
||||
func readIdentity(ctx context.Context, open *stores, in *compose.Input) error {
|
||||
needs := false
|
||||
for _, m := range in.Catalogue {
|
||||
needs = needs || m.Certificate != nil
|
||||
}
|
||||
if !needs {
|
||||
return nil
|
||||
}
|
||||
ident, err := open.Identity(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certificate, known, err := ident.KnownAuthority(ctx); err != nil {
|
||||
return err
|
||||
} else if known {
|
||||
in.Authority = certificate
|
||||
}
|
||||
nodes, err := open.inventory.Nodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := map[string]string{}
|
||||
for _, n := range nodes {
|
||||
ids[n.Name] = n.ID
|
||||
}
|
||||
for i := range in.Nodes {
|
||||
n := &in.Nodes[i]
|
||||
if n.ServingKey, err = ident.ServingKeyOf(ctx, ids[n.Name]); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.ServingKey == "" {
|
||||
continue
|
||||
}
|
||||
if certificate, known, err := ident.KeptCertificate(ctx, overlay.InternalName(n.Name), n.ServingKey); err != nil {
|
||||
return err
|
||||
} else if known {
|
||||
n.Certificate = certificate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readCredentials is how each pair credential a node takes from elsewhere stands, the credentials made for each provider's
|
||||
// consumers, the own secrets, the vault's boxes and the bus. It reads and never makes: a credential a send would make is
|
||||
// left out (the composition's would-make), a credential only a person gives and nobody gave is not-given, and one the send
|
||||
// would be refused is refused with the controller's words.
|
||||
func readCredentials(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved) (
|
||||
[]inventory.PairCredential, error) {
|
||||
var wouldMake []inventory.PairCredential
|
||||
inv := open.inventory
|
||||
shelf := in.Catalogue
|
||||
nodeOf := map[string]compose.Node{}
|
||||
for _, n := range in.Nodes {
|
||||
nodeOf[n.Name] = n
|
||||
}
|
||||
names := make([]string, 0, len(plans))
|
||||
for name := range plans {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
resolved := plans[name]
|
||||
nodeShelf := shelfWith(shelf, nodeOf[name].Kept)
|
||||
for _, need := range resolved.Plan.Needs {
|
||||
if need.ByRecord {
|
||||
continue
|
||||
}
|
||||
p := inventory.PairCredential{Name: need.Name, Consumer: name, ConsumerModule: need.For,
|
||||
Provider: need.From, Local: need.Local}
|
||||
if module, outside := issuedOutsideBy(nodeShelf, need); outside {
|
||||
p.Kind, p.ProviderModule = inventory.PairOutside, module
|
||||
} else if need.SharedOwn != "" {
|
||||
p.Kind, p.Own = inventory.PairShared, need.SharedOwn
|
||||
p.ProviderModule = sharedProviderModule(resolved.Plan, shelf, need)
|
||||
}
|
||||
state, err := inv.PairCredentialState(ctx, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := compose.Credential{Provision: need.Name, Consumer: name, ConsumerModule: need.For,
|
||||
Provider: need.From, Local: need.Local}
|
||||
switch state.State {
|
||||
case inventory.PairHeld:
|
||||
c.State, c.Sealed = compose.CredentialHeld, state.Sealed
|
||||
case inventory.PairNotGiven:
|
||||
c.State, c.Why = compose.CredentialNotGiven, state.Why
|
||||
case inventory.PairRefused:
|
||||
c.State, c.Why = compose.CredentialRefused, state.Why
|
||||
default:
|
||||
// Left out: a need with no entry is the credential the next send makes, and perform makes it.
|
||||
wouldMake = append(wouldMake, p)
|
||||
continue
|
||||
}
|
||||
in.Credentials = append(in.Credentials, c)
|
||||
}
|
||||
}
|
||||
|
||||
// The credentials made for each provider's consumers, so the grants it must create are read from them.
|
||||
for _, n := range in.Nodes {
|
||||
rows, err := inv.SecretsFrom(ctx, n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range rows {
|
||||
in.Issued = append(in.Issued, compose.Issued{Provision: s.Name, Consumer: s.Consumer,
|
||||
ConsumerModule: s.ConsumerModule, Local: s.Local, Provider: n.Name, ForProvider: s.ForProvider})
|
||||
}
|
||||
}
|
||||
own, err := inv.OwnSecretRows(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range own {
|
||||
in.OwnSecrets = append(in.OwnSecrets, compose.OwnSecretRow{Node: r.Node, Module: r.Module, Name: r.Name,
|
||||
Origin: r.Origin, SealedTo: r.SealedTo, Sealed: r.Sealed})
|
||||
}
|
||||
for _, n := range in.Nodes {
|
||||
boxes, err := inv.KeptCoreBoxes(ctx, n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for module, byName := range boxes {
|
||||
for name, b := range byName {
|
||||
in.KeptCoreBoxes = append(in.KeptCoreBoxes, compose.CoreBox{Node: n.Name, Module: module, Name: name,
|
||||
Generation: b.Generation, SealedTo: b.SealedTo, Box: b.Box})
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(in.KeptCoreBoxes, func(i, j int) bool {
|
||||
a, b := in.KeptCoreBoxes[i], in.KeptCoreBoxes[j]
|
||||
if a.Node != b.Node {
|
||||
return a.Node < b.Node
|
||||
}
|
||||
if a.Module != b.Module {
|
||||
return a.Module < b.Module
|
||||
}
|
||||
return a.Name < b.Name
|
||||
})
|
||||
|
||||
// The bus: its user list as the controller composes it, for the machine that runs it, and who the mesh has minted a
|
||||
// credential for.
|
||||
var bus []catalogue.Manifest
|
||||
for _, m := range shelf {
|
||||
if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") {
|
||||
bus = append(bus, m)
|
||||
}
|
||||
}
|
||||
if len(bus) > 0 {
|
||||
list, missing, err := busUserList(ctx, inv, bus)
|
||||
if err != nil && !strings.Contains(err.Error(), "not one user has a credential") {
|
||||
return nil, err
|
||||
}
|
||||
in.BusUsers, in.BusMissing = list, missing
|
||||
}
|
||||
if in.BusMinted, err = inv.BusMinted(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.BusMemberships, err = inv.BusMemberships(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wouldMake, nil
|
||||
}
|
||||
|
||||
// readBuilds is the build half of the Input: every repository the mesh built, each module's current build, and the build made
|
||||
// from a commit of a module for the machines whose node-engine reads them.
|
||||
func readBuilds(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved, shape inputShape) error {
|
||||
inv := open.inventory
|
||||
held, err := inv.Held(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for repository := range held {
|
||||
in.Built = append(in.Built, repository)
|
||||
}
|
||||
sort.Strings(in.Built)
|
||||
in.CurrentBuilds = make(map[string]string, len(shape.current))
|
||||
for module, b := range shape.current {
|
||||
in.CurrentBuilds[module] = b.Commit
|
||||
}
|
||||
// The build made from each commit a declaration may carry, for the machines that read a build per module.
|
||||
for _, n := range in.Nodes {
|
||||
if !n.ReadsModules {
|
||||
continue
|
||||
}
|
||||
resolved, ok := plans[n.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, m := range resolved.Plan.Modules {
|
||||
for _, commit := range []string{shape.current[m.Module].Commit, shape.kept[n.Name][m.Module], shape.sent[n.Name][m.Module]} {
|
||||
if commit == "" {
|
||||
continue
|
||||
}
|
||||
if _, done := in.BuildIDs[m.Module][commit]; done {
|
||||
continue
|
||||
}
|
||||
build, err := inv.BuildOf(ctx, m.Module, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.BuildIDs == nil {
|
||||
in.BuildIDs = map[string]map[string]string{}
|
||||
}
|
||||
if in.BuildIDs[m.Module] == nil {
|
||||
in.BuildIDs[m.Module] = map[string]string{}
|
||||
}
|
||||
in.BuildIDs[m.Module][commit] = build
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shelfWith is the shelf with each module a node keeps at the build it runs put in place of the current one: a copy.
|
||||
func shelfWith(shelf map[string]catalogue.Manifest, kept map[string]catalogue.Manifest) map[string]catalogue.Manifest {
|
||||
if len(kept) == 0 {
|
||||
return shelf
|
||||
}
|
||||
out := make(map[string]catalogue.Manifest, len(shelf)+len(kept))
|
||||
for name, m := range shelf {
|
||||
out[name] = m
|
||||
}
|
||||
for name, m := range kept {
|
||||
out[name] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sharedProviderModule is which module answers a shared credential on the providing node: the one in the plan, else the first
|
||||
// the catalogue names (providerModuleOf, in an order that does not move between calls).
|
||||
func sharedProviderModule(plan catalogue.Resolution, shelf map[string]catalogue.Manifest, n catalogue.Needed) string {
|
||||
for _, m := range plan.Modules {
|
||||
if _, shared := m.SharedCredentialOf(n.Name); shared {
|
||||
return m.Module
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(shelf))
|
||||
for name := range shelf {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
if _, shared := shelf[name].SharedCredentialOf(n.Name); shared {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// tableState is every table's rows and every sequence's last value, as the adapter's reads must leave them.
|
||||
func tableState(t *testing.T, inv *inventory.Inventory) map[string]string {
|
||||
t.Helper()
|
||||
out := map[string]string{}
|
||||
for _, row := range inv.QueryForTest(t, `select table_name from information_schema.tables
|
||||
where table_schema = 'public' and table_type = 'BASE TABLE' order by table_name`) {
|
||||
name := strings.Trim(row[0], `"`)
|
||||
count := inv.QueryForTest(t, fmt.Sprintf(`select count(*)::text from %q`, name))
|
||||
out["rows:"+name] = fmt.Sprint(count)
|
||||
digest := inv.QueryForTest(t, fmt.Sprintf(`select coalesce(md5(string_agg(t::text, '|' order by t::text)), '') from %q t`, name))
|
||||
out["content:"+name] = fmt.Sprint(digest)
|
||||
}
|
||||
for _, row := range inv.QueryForTest(t, `select sequencename, coalesce(last_value::text, '') from pg_sequences
|
||||
where schemaname = 'public' order by sequencename`) {
|
||||
out["sequence:"+row[0]] = row[1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// 1382/AC4 and 1383/AC6: building the Input writes nothing. The store's rows, their contents and its sequences are the same
|
||||
// before and after, on a mesh whose credentials are not made, whose ports are not given and which keeps a recorded build.
|
||||
func TestBuildingTheInputWritesNothing(t *testing.T) {
|
||||
open, _ := aMeshWithSecrets(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
// A consumer whose credential nothing has made yet, on a mesh that already composed.
|
||||
register(t, open, catalogue.Manifest{Module: "reports", Version: "1", Requires: []string{"s3-bucket"},
|
||||
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/reports/config.json", "mode": "0600",
|
||||
"content": "{}", "merge": "json"}}})
|
||||
if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := tableState(t, inv)
|
||||
if _, err := readComposeInput(ctx, open); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after := tableState(t, inv)
|
||||
var moved []string
|
||||
for k, v := range after {
|
||||
if before[k] != v {
|
||||
moved = append(moved, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(moved)
|
||||
if len(moved) > 0 {
|
||||
t.Fatalf("building the Input wrote: %v", moved)
|
||||
}
|
||||
}
|
||||
|
||||
// a consumer of the s3-bucket aMeshWithSecrets' provider offers, with a file the credential goes into.
|
||||
func aConsumerOfTheBucket(name string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: name, Version: "1", Requires: []string{"s3-bucket"},
|
||||
Secrets: map[string]string{"s3-bucket": "/etc/" + name + "/s3"},
|
||||
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/" + name + "/config.json", "mode": "0600",
|
||||
"content": "{}", "merge": "json"}}}
|
||||
}
|
||||
|
||||
// 1383/AC1: of a provider's three consumers, two have their credentials made and one has not: Issued holds the two rows, with
|
||||
// the copy sealed to the provider, the third is a credential the next send makes, and no row was inserted for it (planFor
|
||||
// inserted it, plan.go: SecretFor in every mode).
|
||||
func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) {
|
||||
open, _ := aMeshWithSecrets(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
register(t, open, aConsumerOfTheBucket("reports"))
|
||||
if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The controller's own planning makes the credentials of files and reports.
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, aConsumerOfTheBucket("metrics"))
|
||||
if _, err := inv.Assign(ctx, "laptop", "metrics"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := tableState(t, inv)
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var rows []compose.Issued
|
||||
for _, r := range in.Issued {
|
||||
if r.Provider == "anchor" && r.Provision == "s3-bucket" {
|
||||
rows = append(rows, r)
|
||||
if r.ForProvider == "" {
|
||||
t.Errorf("the row of %s carries no copy sealed to the provider", r.ConsumerModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("Issued holds %d row(s) for anchor's s3-bucket, want files and reports: %+v", len(rows), rows)
|
||||
}
|
||||
var would []string
|
||||
for _, p := range in.wouldMake {
|
||||
would = append(would, p.ConsumerModule)
|
||||
}
|
||||
if len(would) != 1 || would[0] != "metrics" {
|
||||
t.Fatalf("the credentials the next send makes are %v, want metrics", would)
|
||||
}
|
||||
for _, c := range in.Credentials {
|
||||
if c.ConsumerModule == "metrics" {
|
||||
t.Errorf("a credential nobody holds has an entry: %+v", c)
|
||||
}
|
||||
}
|
||||
after := tableState(t, inv)
|
||||
for k, v := range after {
|
||||
if before[k] != v {
|
||||
t.Errorf("reading the Input moved %s", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1383/AC2: a credential only a person gives that nobody gave is not given, and so is one given under a key the machine no
|
||||
// longer has; a credential given by a person for the pair and sealed to a key one end no longer has is refused: in the words
|
||||
// plan.go and secrets.go use.
|
||||
func TestACredentialOnlyAPersonGivesIsNotGivenAndAGivenOneUnderAnOldKeyIsSaidSo(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
register(t, open, catalogue.Manifest{Module: "indexers", Version: "1", Provides: []catalogue.Offer{{
|
||||
Name: "indexer-api", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}})
|
||||
register(t, open, catalogue.Manifest{Module: "grabber", Version: "1", Requires: []string{"indexer-api"},
|
||||
Secrets: map[string]string{"indexer-api": "/etc/grabber/key"}})
|
||||
register(t, open, aConsumerOfTheBucket("ledger"))
|
||||
register(t, open, catalogue.Manifest{Module: "objects", Version: "1", Provides: catalogue.FromAnywhere("s3-bucket")})
|
||||
for _, a := range [][2]string{{"anchor", "indexers"}, {"laptop", "grabber"}, {"anchor", "objects"}, {"laptop", "ledger"}} {
|
||||
if _, err := inv.Assign(ctx, a[0], a[1]); err != nil {
|
||||
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
|
||||
}
|
||||
}
|
||||
stateOf := func(provision, consumerModule string) (compose.Credential, bool) {
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range in.Credentials {
|
||||
if c.Provision == provision && c.ConsumerModule == consumerModule {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return compose.Credential{}, false
|
||||
}
|
||||
|
||||
c, found := stateOf("indexer-api", "grabber")
|
||||
if !found || c.State != compose.CredentialNotGiven || !strings.Contains(c.Why, "nobody gave it") {
|
||||
t.Fatalf("a credential nobody gave reads as %+v", c)
|
||||
}
|
||||
if err := inv.AcceptSecretForPair(ctx, "indexer-api", "laptop", "grabber", "anchor", "", "the-key"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialHeld || c.Sealed == "" {
|
||||
t.Fatalf("a credential a person gave reads as %+v", c)
|
||||
}
|
||||
// The consumer's machine generated a key of its own since.
|
||||
if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialNotGiven ||
|
||||
!strings.Contains(c.Why, "a sealing key one end no longer has") {
|
||||
t.Fatalf("a credential given under a key the machine no longer has reads as %+v", c)
|
||||
}
|
||||
|
||||
// A credential for a pair, given by a person, under a key one end has since replaced: the mesh cannot make another.
|
||||
if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.AcceptSecretForPair(ctx, "s3-bucket", "laptop", "ledger", "anchor", "", "the-bucket-key"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, found := stateOf("s3-bucket", "ledger"); !found || c.State != compose.CredentialRefused ||
|
||||
!strings.Contains(c.Why, "was accepted from a person, and a sealing key at one end has changed since") {
|
||||
t.Fatalf("a credential given for the pair under an old key reads as %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
// 1383/AC3: the own secrets carry their origin, the key they are sealed to and the sealed value; the vault's kept boxes, the
|
||||
// licences (with their keys given, so an empty key means the mesh holds none), the users the bus has minted and the
|
||||
// memberships equal what the inventory answers.
|
||||
func TestTheAdaptersInputCarriesTheOwnSecretsTheBoxesTheLicencesAndTheBus(t *testing.T) {
|
||||
open, _ := aMeshWithSecrets(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
key, err := inv.SealingKeyOf(ctx, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed, err := inv.SecretForModule(ctx, "anchor", "objects", "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
box := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`)
|
||||
if err := inv.KeepCoreBox(ctx, "anchor", inventory.CoreBox{Module: "objects", Name: "admin", Generation: 4,
|
||||
SealedTo: key, Box: box}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SeedBusUser(ctx, inventory.BusUser{Username: "anchor.objects", Kind: "module", Node: "anchor",
|
||||
Module: "objects"}, "a-password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.PutBusMembership(ctx, "laptop", "sealed-membership"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held, err := open.Licences(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := held.Add(ctx, "claude", "static", map[string]any{"model": "x"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var own *compose.OwnSecretRow
|
||||
for i := range in.OwnSecrets {
|
||||
if r := &in.OwnSecrets[i]; r.Node == "anchor" && r.Module == "objects" && r.Name == "admin" {
|
||||
own = r
|
||||
}
|
||||
}
|
||||
if own == nil || own.Origin != "made" || own.SealedTo != key || own.Sealed != sealed {
|
||||
t.Errorf("the own secret reads as %+v; the inventory holds %q sealed to %q", own, sealed, key)
|
||||
}
|
||||
var kept *compose.CoreBox
|
||||
for i := range in.KeptCoreBoxes {
|
||||
if b := &in.KeptCoreBoxes[i]; b.Node == "anchor" && b.Module == "objects" && b.Name == "admin" {
|
||||
kept = b
|
||||
}
|
||||
}
|
||||
if kept == nil || kept.Generation != 4 || kept.SealedTo != key || string(kept.Box) != string(box) {
|
||||
t.Errorf("the kept box reads as %+v", kept)
|
||||
}
|
||||
if got := strings.Join(in.BusMinted, ","); !strings.Contains(got, "anchor.objects") {
|
||||
t.Errorf("BusMinted is %v", in.BusMinted)
|
||||
}
|
||||
if in.BusMemberships["laptop"] != "sealed-membership" {
|
||||
t.Errorf("BusMemberships is %v", in.BusMemberships)
|
||||
}
|
||||
if len(in.Licences) != 1 || in.Licences[0].Name != "claude" || in.Licences[0].Keys == nil {
|
||||
t.Errorf("Licences read as %+v", in.Licences)
|
||||
}
|
||||
}
|
||||
|
||||
// 1383/AC4: the bus's user list is what busUserList gives for the machine that runs the bus, and the Input carries none when
|
||||
// no module of the shelf runs the bus.
|
||||
func TestTheBusUsersAreWhatTheControllerComposesForTheMachineThatRunsTheBus(t *testing.T) {
|
||||
open, _ := aMeshWithSecrets(t)
|
||||
ctx := t.Context()
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if in.BusUsers != "" || len(in.BusMissing) != 0 {
|
||||
t.Fatalf("a mesh whose shelf runs no bus carries a bus user list: %q %v", in.BusUsers, in.BusMissing)
|
||||
}
|
||||
bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf",
|
||||
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
|
||||
register(t, open, bus)
|
||||
if err := open.inventory.SeedBusUser(ctx, inventory.BusUser{Username: "controller", Kind: "controller"}, "pw"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in, err = readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, missing, _ := busUserList(ctx, open.inventory, []catalogue.Manifest{bus})
|
||||
if in.BusUsers != list || fmt.Sprint(in.BusMissing) != fmt.Sprint(missing) {
|
||||
t.Errorf("the Input carries %q (%v); busUserList gives %q (%v)", in.BusUsers, in.BusMissing, list, missing)
|
||||
}
|
||||
}
|
||||
|
||||
// 1383/AC5: Built, CurrentBuilds and BuildIDs equal the inventory's Held, CurrentBuilds and BuildOf, and the serving key and
|
||||
// the certificate are the machine's.
|
||||
func TestTheAdaptersInputCarriesTheBuildsAndTheMachinesKey(t *testing.T) {
|
||||
open, _ := aMeshWithSecrets(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held, err := inv.Held(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var wantBuilt []string
|
||||
for repository := range held {
|
||||
wantBuilt = append(wantBuilt, repository)
|
||||
}
|
||||
sort.Strings(wantBuilt)
|
||||
if fmt.Sprint(in.Built) != fmt.Sprint(wantBuilt) && !(len(in.Built) == 0 && len(wantBuilt) == 0) {
|
||||
t.Errorf("Built is %v, the inventory's %v", in.Built, wantBuilt)
|
||||
}
|
||||
current, err := inv.CurrentBuilds(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for module, b := range current {
|
||||
if in.CurrentBuilds[module] != b.Commit {
|
||||
t.Errorf("CurrentBuilds[%s] is %q, the inventory's %q", module, in.CurrentBuilds[module], b.Commit)
|
||||
}
|
||||
}
|
||||
for module, byCommit := range in.BuildIDs {
|
||||
for commit, build := range byCommit {
|
||||
if want, _ := inv.BuildOf(ctx, module, commit); want != build {
|
||||
t.Errorf("BuildIDs[%s][%s] is %q, the inventory's %q", module, commit, build, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// The reads of what Certify and EstablishAuthority would keep, without keeping anything (design 47 R4b, issue 1383): the
|
||||
// controller's adapter from its stores to mesh-sdk's composition input fills the certificate and the authority it holds, and
|
||||
// a machine whose certificate is not held yet is composed with a stand-in until the next send issues it.
|
||||
|
||||
// KnownAuthority is the mesh's public certificate when it has an authority, and false when none was established yet. It
|
||||
// establishes nothing.
|
||||
func (i *Identity) KnownAuthority(ctx context.Context) (string, bool, error) {
|
||||
held, err := i.authority(ctx)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return held.Certificate, true, nil
|
||||
}
|
||||
|
||||
// KeptCertificate is the certificate Certify would answer without signing, for a node's internal name and serving key: the
|
||||
// one kept for the key while it is for that name, binds that key and has life left. False when Certify would sign a fresh
|
||||
// one. It writes nothing.
|
||||
func (i *Identity) KeptCertificate(ctx context.Context, name, servingKey string) (string, bool, error) {
|
||||
public, err := base64.StdEncoding.DecodeString(servingKey)
|
||||
if err != nil || len(public) != ed25519.PublicKeySize {
|
||||
return "", false, nil
|
||||
}
|
||||
var kept *string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select certificate from node_key where serving_key = $1 and revoked is null`, servingKey).Scan(&kept)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", false, err
|
||||
}
|
||||
if kept != nil && stillStands(*kept, name, public) {
|
||||
return *kept, true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/secrets"
|
||||
)
|
||||
|
||||
// The reads of what a composition would make or hold, in one place and writing nothing (design 47 R4b, issue 1383): the
|
||||
// controller's adapter from its stores to mesh-sdk's composition input asks them once for the whole mesh, where the
|
||||
// composition's own reads (SecretFor, SharedSecretFor, ModuleSecretIfIssued, GivenOwnSecret) ask for one pair or one
|
||||
// secret and make what is not held.
|
||||
|
||||
// OwnSecretRow is one own secret held on a machine (a module_secret row): who made it (`made`, or `accepted` for a
|
||||
// value a person gave), the sealing key it is sealed to, and the value sealed to that key.
|
||||
type OwnSecretRow struct {
|
||||
Node, Module, Name, Origin, SealedTo, Sealed string
|
||||
}
|
||||
|
||||
// OwnSecretRows is every own secret held on every machine, by machine, module and name. It writes nothing.
|
||||
func (i *Inventory) OwnSecretRows(ctx context.Context) ([]OwnSecretRow, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select n.name, m.module, m.name, m.origin, m.node_key, m.sealed
|
||||
from module_secret m join node n on n.id = m.node
|
||||
order by n.name, m.module, m.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []OwnSecretRow
|
||||
for rows.Next() {
|
||||
var r OwnSecretRow
|
||||
if err := rows.Scan(&r.Node, &r.Module, &r.Name, &r.Origin, &r.SealedTo, &r.Sealed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// PairState is how one pair credential stands, as the composition's reads of it would find it (what SecretFor,
|
||||
// SharedSecretFor and GivenPairSecret answer), without making anything.
|
||||
type PairState struct {
|
||||
// State is one of the Pair constants.
|
||||
State string
|
||||
// Sealed is the consumer's copy as it is sealed to the consumer's key, for a held credential.
|
||||
Sealed string
|
||||
// Why is the controller's words for a credential not given or refused.
|
||||
Why string
|
||||
}
|
||||
|
||||
// The states of a pair credential.
|
||||
const (
|
||||
// PairHeld is a credential held, sealed to the keys both ends have now.
|
||||
PairHeld = "held"
|
||||
// PairWouldMake is a credential the next send makes.
|
||||
PairWouldMake = "would-make"
|
||||
// PairNotGiven is a credential only a person gives (novox/hq ADR 0337), which nobody gave under the keys the ends have now.
|
||||
PairNotGiven = "not-given"
|
||||
// PairRefused is a credential the next send would be refused when it made it, for the reason Why gives.
|
||||
PairRefused = "refused"
|
||||
)
|
||||
|
||||
// PairCredentialKind says which of the controller's reads a pair is answered by.
|
||||
type PairCredentialKind int
|
||||
|
||||
const (
|
||||
// PairPlain is a credential made for the pair (SecretFor).
|
||||
PairPlain PairCredentialKind = iota
|
||||
// PairShared is the provider's one credential sealed to the consumer too (SharedSecretFor; Own names the provider's own secret).
|
||||
PairShared
|
||||
// PairOutside is a credential only a person gives (GivenPairSecret).
|
||||
PairOutside
|
||||
)
|
||||
|
||||
// PairCredential is one pair credential, by the ends it joins.
|
||||
type PairCredential struct {
|
||||
Kind PairCredentialKind
|
||||
Name, Consumer, ConsumerModule, Provider, ProviderModule, Local string
|
||||
// Own is, for a shared credential, the provider's own secret it is.
|
||||
Own string
|
||||
}
|
||||
|
||||
// PairCredentialState says how a pair credential stands now, by the rules the next send applies and without applying
|
||||
// them: nothing is inserted, remade or sealed. A credential the send would make is PairWouldMake, unless the send would be
|
||||
// refused (a machine with no sealing key, a value given and sealed to a key one end no longer has), which is PairRefused
|
||||
// in the send's own words.
|
||||
func (i *Inventory) PairCredentialState(ctx context.Context, p PairCredential) (PairState, error) {
|
||||
switch p.Kind {
|
||||
case PairOutside:
|
||||
return i.outsideState(ctx, p)
|
||||
case PairShared:
|
||||
return i.sharedState(ctx, p)
|
||||
}
|
||||
return i.plainState(ctx, p)
|
||||
}
|
||||
|
||||
func (i *Inventory) outsideState(ctx context.Context, p PairCredential) (PairState, error) {
|
||||
got, err := i.GivenPairSecret(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.ProviderModule, p.Local)
|
||||
var notGiven *NotGivenError
|
||||
switch {
|
||||
case errors.As(err, ¬Given):
|
||||
return PairState{State: PairNotGiven, Why: notGiven.Why}, nil
|
||||
case err != nil:
|
||||
return PairState{}, err
|
||||
}
|
||||
return PairState{State: PairHeld, Sealed: got.ForConsumer}, nil
|
||||
}
|
||||
|
||||
func (i *Inventory) plainState(ctx context.Context, p PairCredential) (PairState, error) {
|
||||
consumerKey, err := i.SealingKeyOf(ctx, p.Consumer)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
providerKey, err := i.SealingKeyOf(ctx, p.Provider)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
consumerNode, err := i.NodeByName(ctx, p.Consumer)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
providerNode, err := i.NodeByName(ctx, p.Provider)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
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`,
|
||||
p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local).
|
||||
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return PairState{}, err
|
||||
}
|
||||
if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey {
|
||||
return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil
|
||||
}
|
||||
if err == nil && held.Origin == OriginAccepted {
|
||||
return PairState{State: PairRefused, Why: fmt.Sprintf(
|
||||
"%s's %q credential from %s was accepted from a person, and a sealing key at one end "+
|
||||
"has changed since. The mesh cannot re-seal a value it does not hold: accept it "+
|
||||
"again with `secret accept %s %s %s --provider %s%s`",
|
||||
p.ConsumerModule, p.Name, p.Provider, p.Consumer, p.ConsumerModule, p.Name, p.Provider, localFlag(p.Local))}, nil
|
||||
}
|
||||
// What making one would meet: the operator's key and the two sealing keys, by the same call that seals them.
|
||||
operator, err := i.OperatorKey(ctx)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
if _, _, merr := secrets.MakeWithOperator(consumerKey, providerKey, operator); merr != nil {
|
||||
return PairState{State: PairRefused, Why: merr.Error()}, nil
|
||||
}
|
||||
return PairState{State: PairWouldMake}, nil
|
||||
}
|
||||
|
||||
func (i *Inventory) sharedState(ctx context.Context, p PairCredential) (PairState, error) {
|
||||
consumerKey, err := i.SealingKeyOf(ctx, p.Consumer)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
consumerNode, err := i.NodeByName(ctx, p.Consumer)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
providerNode, err := i.NodeByName(ctx, p.Provider)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
providerKey, err := i.SealingKeyOf(ctx, p.Provider)
|
||||
if err != nil {
|
||||
return PairState{}, err
|
||||
}
|
||||
if consumerKey == "" || providerKey == "" {
|
||||
return PairState{State: PairRefused, Why: fmt.Sprintf("%s and %s both need a sealing key before %s can be shared",
|
||||
p.Consumer, p.Provider, p.Name)}, nil
|
||||
}
|
||||
var ownGeneration, ownOrigin, ownKey *string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select generation, origin, node_key from module_secret where node = $1 and module = $2 and name = $3`,
|
||||
providerNode.ID, p.ProviderModule, p.Own).Scan(&ownGeneration, &ownOrigin, &ownKey)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return PairState{}, err
|
||||
}
|
||||
var held Secret
|
||||
var pairGeneration *string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select for_consumer, for_provider, consumer_key, provider_key, origin, generation from secret
|
||||
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
|
||||
p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local).
|
||||
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin, &pairGeneration)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return PairState{}, err
|
||||
}
|
||||
current := ownGeneration != nil && pairGeneration != nil && *ownGeneration == *pairGeneration &&
|
||||
held.ConsumerKey == consumerKey && held.ProviderKey == providerKey && ownKey != nil && *ownKey == providerKey
|
||||
if current {
|
||||
return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil
|
||||
}
|
||||
if _, outside, err := i.ownIssuedOutside(ctx, p.ProviderModule, p.Own); err != nil {
|
||||
return PairState{}, err
|
||||
} else if outside && (ownOrigin == nil || *ownOrigin != OriginAccepted) {
|
||||
return PairState{State: PairNotGiven,
|
||||
Why: "nobody gave " + p.ProviderModule + "'s own secret " + p.Own + ", which is issued outside the mesh"}, nil
|
||||
}
|
||||
if ownOrigin != nil && *ownOrigin == OriginAccepted {
|
||||
return PairState{State: PairRefused, Why: fmt.Sprintf(
|
||||
"%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+
|
||||
"mesh, which cannot seal it to a consumer that binds later (ADR 0158): `secret accept %s %s %s` "+
|
||||
"again, which seals it to every current consumer",
|
||||
p.ConsumerModule, p.Consumer, p.Name, p.Provider, p.ProviderModule, p.Own, p.Provider, p.ProviderModule, p.Own)}, nil
|
||||
}
|
||||
return PairState{State: PairWouldMake}, nil
|
||||
}
|
||||
|
||||
// BusMinted is the name of every bus user the mesh has minted a password hash for: `<node>.<module>` for a module and
|
||||
// `provisioner.<node>` for a machine's provisioner client.
|
||||
func (i *Inventory) BusMinted(ctx context.Context) ([]string, error) {
|
||||
kept, err := i.BusUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]string, 0, len(kept))
|
||||
for name := range kept {
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user