Let the controller ask an issued credential's tool in its own name, and seal it again over a remade pair (hq issue 526, ADR 0305)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/526-a-provider-delivers-what-its-software-issued delivered: every member is delivered
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/526-a-provider-delivers-what-its-software-issued delivered: every member is delivered
The bus left the controller out of the caller-named grants, so issued could never reach plex's tool; the controller is now granted exactly the tools of offers whose software issues the credential, composed from the records, so the installer's first user list is unchanged. A pair remade on a key change or accepted by hand kept the issued stamp and was never sealed again; those writes now clear it.
This commit is contained in:
@@ -3,6 +3,7 @@ package inventory
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
@@ -170,6 +171,9 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
Checks: catalogue.HealthChecks(m),
|
||||
// And whether it runs as an account of its own (novox/hq ADR 0259 §8).
|
||||
RunsAs: m.RunsAs,
|
||||
// And the tools that answer a credential its software issued, which the controller asks in its own
|
||||
// name (novox/hq ADR 0305).
|
||||
Issued: issuedTools(m),
|
||||
}
|
||||
// Whether it can be given an account at all: delivered as its own secret named broker, so one
|
||||
// that declares none has nowhere to read it (novox/hq issue 195).
|
||||
@@ -197,6 +201,17 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
return d
|
||||
}
|
||||
|
||||
// issuedTools are the tools through which a module answers the credentials its software issues (novox/hq ADR 0305).
|
||||
func issuedTools(m catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, o := range m.Provides {
|
||||
if tool, issued := m.IssuedCredentialOf(o.Name); issued && !slices.Contains(out, tool) {
|
||||
out = append(out, tool)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// bucketsOf is the state a module keeps, as the bus holds it.
|
||||
func bucketsOf(m catalogue.Manifest) []broker.Bucket {
|
||||
var out []broker.Bucket
|
||||
|
||||
@@ -122,3 +122,57 @@ func TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain(t *testing.T) {
|
||||
t.Fatal("an empty value was kept")
|
||||
}
|
||||
}
|
||||
|
||||
// A consumer whose sealing key changed holds a value the mesh made in place of the issued one, and the next ask
|
||||
// seals the issued value to it again; so does a pair a person accepted over it (novox/hq ADR 0305 §5, the review
|
||||
// of mesh-controller #239).
|
||||
func TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance(t *testing.T) {
|
||||
inv, ctx := twoNodesWithKeys(t)
|
||||
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "kometa", Version: "1", Requires: []string{"plex-api"},
|
||||
Secrets: map[string]string{"plex-api": "/run/plex-api.secret"}}, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.SecretFor(ctx, "plex-api", "consumer", "kometa", "provider", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if kept, err := inv.KeepIssuedSecret(ctx, "plex-api", "provider", "the-servers-own-token"); err != nil || !kept.Changed {
|
||||
t.Fatalf("the first ask did not seal: %+v %v", kept, err)
|
||||
}
|
||||
|
||||
// The consumer rejoins with a new key: the mesh remakes the pair, a value the software refuses.
|
||||
newKey, opens := aSealingKey(t)
|
||||
cn, _ := inv.NodeByName(ctx, "consumer")
|
||||
if err := inv.RecordSealingKey(ctx, cn.ID, newKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remade, err := inv.SecretFor(ctx, "plex-api", "consumer", "kometa", "provider", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if remade.Origin != OriginMade {
|
||||
t.Fatalf("a value the mesh remade says it came from %q", remade.Origin)
|
||||
}
|
||||
again, err := inv.KeepIssuedSecret(ctx, "plex-api", "provider", "the-servers-own-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !again.Changed || len(again.Sealed) != 1 {
|
||||
t.Fatalf("the issued value was not sealed again after the key changed: %+v", again)
|
||||
}
|
||||
got, _ := inv.SecretFor(ctx, "plex-api", "consumer", "kometa", "provider", "")
|
||||
if opened, ok := opens(got.ForConsumer); !ok || string(opened) != "the-servers-own-token" {
|
||||
t.Fatal("the consumer does not hold the issued value under its new key")
|
||||
}
|
||||
|
||||
// A person accepts another value over it: the next ask seals the software's again.
|
||||
if err := inv.AcceptSecretForPair(ctx, "plex-api", "consumer", "kometa", "provider", "", "a-typed-copy"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, err := inv.KeepIssuedSecret(ctx, "plex-api", "provider", "the-servers-own-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !after.Changed {
|
||||
t.Fatal("an accepted value over the issued one was read as still holding it")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ const (
|
||||
// **Remade when either end's sealing key changes.** A node that rejoined generated a new key and
|
||||
// can no longer open what was sealed to the old one, so keeping the blob would deliver something
|
||||
// unreadable for ever. The new secret reaches both ends in the same push, which is the only
|
||||
// moment they can be changed together.
|
||||
// moment they can be changed together. A remade value is the mesh's, whatever the row held: its
|
||||
// origin says made and it carries no stamp, so a value a provider's software issued is sealed to it
|
||||
// again at the provider's next ask (novox/hq ADR 0305 §5) rather than read as still held.
|
||||
func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModule, provider, local string) (
|
||||
Secret, error) {
|
||||
consumerKey, err := i.SealingKeyOf(ctx, consumer)
|
||||
@@ -121,7 +123,8 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
|
||||
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
|
||||
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
|
||||
created_at = now(),
|
||||
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
||||
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key,
|
||||
origin = 'made', generation = null`,
|
||||
name, consumerNode.ID, consumerModule, providerNode.ID,
|
||||
made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey, forOperator, operatorKey, local)
|
||||
if err != nil {
|
||||
@@ -202,7 +205,7 @@ func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, con
|
||||
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,
|
||||
created_at = now(), origin = excluded.origin, generation = null,
|
||||
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
||||
name, consumerNode.ID, consumerModule, providerNode.ID,
|
||||
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
|
||||
|
||||
Reference in New Issue
Block a user